using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Numerics;
using System.Reflection;
using System.Security.Principal;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;

using C4IT.F4SD.SupportCaseProtocoll.Models;
using C4IT.FASD.Base;
using C4IT.Logging;
using C4IT.Matrix42.WebClient;
using C4IT.XML;

using C4IT_DataHistoryProvider_Base.DataSources;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

using static C4IT.DataHistoryProvider.cDataHistoryCollectorTokenCache;
using static C4IT.FASD.Base.cF4SDHealthCardRawData;
using static C4IT.FASD.Base.cHealthCardTranslator;
using static C4IT.Logging.cLogManager;
using static C4IT.Matrix42.WebClient.cM42WebClient;

namespace C4IT.DataHistoryProvider
{
    public class cDataHistoryCollectorM42Wpm : cDataHistoryCollectorModule, ISearchResultRelationProvider
    {
        public const string constConnectorName = "Matrix42 WPM conntector";
        public const string constLicenseId = "9CE1A6BE-6A0C-4A27-94A5-44AB997B8E62";

        private const string constUrlIsAlive = "m42Services/api/c4itf4sdwebapi/isalive";
        private const string constUrlGetTickets = "m42Services/api/c4itf4sdwebapi/getticketlist?sid={0}&hours={1}&queueoption={2}";
        private const string constUrlGetTicketsQueuesParam = "&queues={0}";
        private const string constUrlGetTicketDetails = "m42Services/api/c4itf4sdwebapi/getticketdetails?objectid={0}";
        private const string constUrlGetTicketHistory = "m42Services/api/c4itf4sdwebapi/gettickethistory?objectid={0}";
        private const string constUrlGetCreateTicket = "m42Services/api/c4itf4sdwebapi/getdirectlinkcreateticket?sid={0}{1}";
        private const string constUrlGetCreateTicketAssetParam = "&assetname={0}";
        private const string constUrlUpdateSolution = "m42Services/api/c4itf4sdwebapi/updateactivitysolution/{0}/";
        private const string constUrlGetPickupValues = "m42Services/api/c4itf4sdwebapi/getpickup/{0}?group={1}";
        private const string constUrlGetDataQueryRelationItems = "m42Services/api/dataquery/relationitems";
        private const string constUrlGetDataQueryRelationItemsCount = "m42Services/api/dataquery/relationitems/count";
        private const string constUrlCreateTicket = "m42Services/api/ticket/create?activitytype={0}";
        private const string constUrlActivityGetQuickCallTemplateInfo = "m42Services/api/activity/quickcalltemplateinfo/{0}";
        private const string constUrlCreateJournalEntry = "m42Services/api/journal/Add";
        private const string constUrlFragmentsGetList = "m42Services/api/data/fragments/{0}?where={1}&columns={2}&pagesize={3}&pagenumber={4}&sort={5}";
        private const string constUrlObjectsGetObject = "m42Services/api/data/objects/{0}/{1}?full={2}";
        private const string constUrlObjectsUpdateObject = "m42Services/api/data/objects/{0}?full={1}";
        private const string constUrlTicketClose = "m42Services/api/ticket/close";
        private const string constUrlActivityPause = "m42Services/api/activity/Pause";
        private const string constUrlActivityForward = "m42Services/api/activity/Forward";
        private const string constUrlActivityTrackWorkingTime = "m42Services/api/activity/trackworkingtime";
        private const string constUrlTicketTransform = "m42Services/api/ticket/transform";
        private const string constUrlActivityReopen = "m42Services/api/activity/reopen";
        private const string constUrlGenerateApiTokenForMe = "m42Services/api/apitoken/generateapitokenforme";
        private const string constUrlGenerateAccessTokenFromApiToken = "m42Services/api/apitoken/generateaccesstokenfromapitoken";

        [Obsolete]
        private const string constUrlGetRoleMeberships = "m42services/api/c4itf4sdwebapi/getrolememberships/?sid={0}";

        [Obsolete]
        private const string constUrlGetTicketOverviewCounts = "m42Services/api/c4itf4sdwebapi/getticketoverviewcounts";

        [Obsolete]
        private const string constUrlGetTicketOverviewCountsByRoles = "m42Services/api/c4itf4sdwebapi/getticketoverviewcountsbyroles";

        [Obsolete]
        private const string constUrlGetTicketOverviewRelations = "m42Services/api/c4itf4sdwebapi/getticketoverviewrelations";

        private const string constActivityTypeTicket = "SPSActivityTypeTicket";
        private const string constActivityTypeIncident = "SPSActivityTypeIncident";
        private const string constActivityTypeServiceRequest = "SPSActivityTypeServiceRequest";

        private static readonly Guid constActivityTypeTicketTypeId = new Guid("13F950A8-F2F8-E611-E182-60E327035D31");


        public const string constTableNameTicketDetails = "M42Wpm-Tickets";
        public const string constTableNameTicketHistory = "M42Wpm-Ticket-History";
        public const string constTableNameUserTicket = "M42Wpm-User-NewTicket";

        private readonly cM42WebClient WebClient = null;
        private Dictionary<string, cM42WebClient> dicWebClients = new Dictionary<string, cM42WebClient>();

        private readonly Timer onlineTimer = null;

        private bool IsOnline = false;

        private Dictionary<WebClientCacheIndex, WebClientCacheEntry> UserWebClientCache = new Dictionary<WebClientCacheIndex, WebClientCacheEntry>();

        private readonly object _ticketOverviewCacheLock = new object();

        [Obsolete]
        private readonly Dictionary<string, TicketOverviewCountCacheEntry> _ticketOverviewPersonalCache = new Dictionary<string, TicketOverviewCountCacheEntry>(StringComparer.OrdinalIgnoreCase);

        [Obsolete]
        private readonly Dictionary<Guid, TicketOverviewCountCacheEntry> _ticketOverviewRoleCache = new Dictionary<Guid, TicketOverviewCountCacheEntry>();

        [Obsolete]
        private readonly Dictionary<string, TicketOverviewRoleListCacheEntry> _ticketOverviewRoleListCache = new Dictionary<string, TicketOverviewRoleListCacheEntry>(StringComparer.OrdinalIgnoreCase);

        private readonly cDataHistoryCollector _collector;

        private GlobalServerSettings _globalServerSettings = new GlobalServerSettings();

        private class GlobalServerSettings
        {
            internal DateTime LastRead = DateTime.MinValue;
            internal bool TicketAndServiceRequestEnabled = true;
        }

        public bool TicketAndServiceRequestsEnabled => _globalServerSettings?.TicketAndServiceRequestEnabled ?? true;

        private static class _OnlineCheckCriticalSection
        {
            public static SemaphoreSlim Semaphore = new SemaphoreSlim(1, 1);
        }

        private static class _LanguageDicWebClientCriticalSection
        {
            public static SemaphoreSlim Semaphore = new SemaphoreSlim(1, 1);
        }

        private static class _GlobalSettingsQueryCriticalSection
        {
            public static SemaphoreSlim Semaphore = new SemaphoreSlim(1, 1);
        }

        private struct WebClientCacheIndex
        {
            public Guid Id;
            public string Language;
        }

        private class WebClientCacheEntry
        {
            public cM42WebClient WebClient;
            public DateTime LastUsed;
            public DateTime ValidUntil;
        }

        private sealed class TicketOverviewCountCacheEntry
        {
            public Dictionary<string, int> Counts { get; set; } = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
            public DateTime ExpiresAtUtc { get; set; }
        }

        private sealed class TicketOverviewRoleListCacheEntry
        {
            public List<Guid> RoleIds { get; set; } = new List<Guid>();
            public DateTime ExpiresAtUtc { get; set; }
        }

        private sealed class TicketOverviewCountsByRolesRequest
        {
            public string Sid { get; set; }
            public List<Guid> RoleGuids { get; set; } = new List<Guid>();
            public List<string> Keys { get; set; } = new List<string>();
            public int QueueOption { get; set; }
            public string Queues { get; set; }
        }

        private sealed class TicketOverviewCountsByRolesResponse
        {
            [JsonProperty("countsByRole")]
            public Dictionary<string, Dictionary<string, int>> CountsByRole { get; set; }
                = new Dictionary<string, Dictionary<string, int>>(StringComparer.OrdinalIgnoreCase);
        }

        public class cM42ApiTokenModel
        {
            public string ApiToken { get; set; }
            public string Name { get; set; }
            public bool? Enabled { get; set; }
            public DateTime? ValidUntil { get; set; }
            public string UserId { get; set; }
        }

        private class cApiTokenRequestModelBase
        {
            public string Name { get; set; }
            public int ExpirationDays { get; set; }
        }

        public cDataHistoryCollectorM42Wpm(cDataHistoryCollector Collector) : base(Collector, enumDataHistoryOrigin.M42Wpm, constConnectorName, constLicenseId)
        {
            if (Collector.InfrastructureConfig.M42Wpm.Server == null)
                return;

            _collector = Collector;

            Collector.RegisterTokenValidator(cF4SDTokenRegistration.enumTokenType.M42Bearer, this);
            Collector.RegisterTokenValidator(cF4SDTokenRegistration.enumTokenType.M42Api, this);

            WebClient = new cM42WebClient(Collector.InfrastructureConfig.M42Wpm.Server);

            if (Collector.autoConnectionCheck)
                onlineTimer = new Timer(onlineTimerCallback, null, 0, 30000);

        }

        private static enumTicketType ConvertActivityToTicketType(string ActivityType, bool TicketAndServiceRequestEnabled)
        {
            if (!TicketAndServiceRequestEnabled)
                return enumTicketType.Ticket;

            switch (ActivityType)
            {
                case constActivityTypeIncident:
                    return enumTicketType.Incident;
                case constActivityTypeServiceRequest:
                    return enumTicketType.ServiceRequest;
                default:
                    return enumTicketType.UnclassifiedTicket;
            }
        }

        private string BuildTicketDeepLink(Guid ticketId, string activityType)
        {
            if (ticketId == Guid.Empty)
                return null;

            var server = Collector.InfrastructureConfig.M42Wpm.Server;

            var viewOptionsJson = $"{{\"embedded\":false,\"objectId\":\"{ticketId}\",\"type\":\"{activityType}\",\"viewType\":\"preview\",\"archived\":0}}";
            var viewOptionsEncoded = Uri.EscapeDataString(viewOptionsJson);

            return $"https://{server}/wm/app-ServiceDesk/?view-options={viewOptionsEncoded}";
        }

        private async Task<cM42WebClient> GetWebClient(cF4sdWebRequestInfo requestInfo, CancellationToken Token)
        {

            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            try
            {
                if (cLogManager.DefaultLogger.IsDebug)
                    LogEntry($"M42 webclient culture: {requestInfo?.Culture?.TextInfo?.CultureName}", LogLevels.Debug);

                if (requestInfo?.Culture == null)
                {
                    if (cLogManager.DefaultLogger.IsDebug) LogEntry($"M42 webclient requested with no culture", LogLevels.Debug);
                    return WebClient;
                }

                await _LanguageDicWebClientCriticalSection.Semaphore.WaitAsync(Token);
                try
                {
                    var _lang = requestInfo.Culture.TextInfo.CultureName;
                    if (dicWebClients.TryGetValue(_lang, out var client))
                    {
                        if (cLogManager.DefaultLogger.IsDebug) LogEntry($"M42 webclient request returned with culture: {client?.UserLanguage}", LogLevels.Debug);
                        return client;
                    }


                    client = new cM42WebClient(Collector.InfrastructureConfig.M42Wpm.Server, _lang);
                    var resLogon = await client.LogonWithApiKeyAsync(Collector.InfrastructureConfig.M42Wpm.Credential.NwCredential.Password);
                    if (resLogon == cM42WebClient.eReason.ok)
                    {
                        dicWebClients[_lang] = client;
                        if (cLogManager.DefaultLogger.IsDebug) LogEntry($"M42 webclient request returned with culture: {client?.UserLanguage}", LogLevels.Debug);
                        return client;
                    }
                    else
                    {
                        LogEntry($"Could not logon to M42 server with culture {_lang}. Reason: {resLogon}", LogLevels.Warning);
                    }
                }
                catch (Exception E)
                {
                    LogException(E);
                }
                finally
                {
                    _LanguageDicWebClientCriticalSection.Semaphore.Release();
                }

            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (CM != null) LogMethodEnd(CM);
            }

            if (cLogManager.DefaultLogger.IsDebug) LogEntry($"M42 webclient requested return with no culture.", LogLevels.Debug);
            return WebClient;
        }

        private async Task<cM42WebClient> GetUserWebClient(cF4sdWebRequestInfo requestInfo, int LogDeep, CancellationToken Token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            try
            {
                lock (requestInfo)
                    if (requestInfo?.M42WebClient != null)
                        return requestInfo.M42WebClient;

                if (requestInfo?.userInfo?.Id is null)
                    return null;

                var _lang = requestInfo.Culture?.TextInfo?.CultureName;
                if (_lang == null)
                    _lang = "en-Us";

                var _index = new WebClientCacheIndex() { Id = requestInfo.userInfo.Id, Language = _lang };
                lock (UserWebClientCache)
                {
                    if (UserWebClientCache.TryGetValue(_index, out var _cacheEntry) && _cacheEntry.ValidUntil < DateTime.UtcNow + TimeSpan.FromMinutes(cDataHistoryCollector.constMinutesUntilTokenIsInvalid))
                    {
                        _cacheEntry.LastUsed = DateTime.UtcNow;
                        lock (requestInfo)
                            requestInfo.M42WebClient = _cacheEntry.WebClient;
                        return _cacheEntry.WebClient;
                    }
                }

                if (Collector.TokenCache == null)
                    return null;

                var _tokenInfo = await Collector.TokenCache.GetAsync(requestInfo.userInfo.Id, cF4SDTokenRegistration.enumTokenType.M42Bearer, requestInfo, LogDeep + 1);

                if (_tokenInfo == null || !_tokenInfo.IsValid())
                    return null;

                var client = new cM42WebClient(Collector.InfrastructureConfig.M42Wpm.Server, _lang);
                client.SetBearerToken(_tokenInfo.secret);
                var _reason = await client.Open();
                if (_reason == eReason.ok)
                {
                    lock (UserWebClientCache)
                    {
                        UserWebClientCache[_index] = new WebClientCacheEntry() { WebClient = client, LastUsed = DateTime.UtcNow, ValidUntil = _tokenInfo.validUntil };
                    }

                    lock (requestInfo)
                        requestInfo.M42WebClient = client;

                    return client;
                }
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (CM != null) LogMethodEnd(CM);
            }

            return null;
        }

        private void CleanupUserWebCache()
        {
            lock (UserWebClientCache)
            {
                var _del = new List<WebClientCacheIndex>();
                foreach (var _entry in UserWebClientCache)
                    if (_entry.Value.LastUsed < DateTime.UtcNow - TimeSpan.FromMinutes(10) || _entry.Value.ValidUntil < DateTime.UtcNow)
                        _del.Add(_entry.Key);
                foreach (var entry in _del)
                    UserWebClientCache.Remove(entry);
            }
        }

        private async Task CheckAndRefreshGlobalServerSettingsAsync(CancellationToken Token)
        {
            if ((DateTime.UtcNow - _globalServerSettings.LastRead) < TimeSpan.FromMinutes(10))
                return;

            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }

            try
            {
                var _strUrl = string.Format(constUrlFragmentsGetList, "SPSGlobalConfigurationClassServiceDesk", string.Empty, "TicketAndServiceRequestEnabled", string.Empty, string.Empty, string.Empty);
                _strUrl = RemoveEmptyQueryParameters(_strUrl);


                // call the api
                var requestInfo = new cF4sdWebRequestInfo("CheckAndRefreshGlobalServerSettings", "-");
                var _wc = await GetWebClient(requestInfo, Token);
                var res = await _wc.HttpEnh.GetAsync(_strUrl);

                if (res?.IsSuccessStatusCode == true)
                {
                    var strJson = await res.Content.ReadAsStringAsync();

                    if (!string.IsNullOrEmpty(strJson))
                    {
                        // get the result as JArray
                        JArray PickupResult = JsonConvert.DeserializeObject<JArray>(strJson);

                        // loop all rows (should be at least one)
                        foreach (var _row in PickupResult)
                        {
                            try
                            {
                                // get the row values from JSON result
                                foreach (var _col in _row)
                                {
                                    if (_col is JProperty prop)
                                        if (prop.Value is JValue val)
                                        {
                                            Debug.WriteLine($"Property: {prop.Name}, Value: {val.Value}");
                                            switch (prop.Name)
                                            {
                                                case "TicketAndServiceRequestEnabled":

                                                    if (val.Type == JTokenType.Integer)
                                                    {
                                                        var _i = val.Value<Int32>();
                                                        _globalServerSettings.TicketAndServiceRequestEnabled = _i != 0;
                                                    }
                                                    break;
                                            }
                                        }
                                }
                            }
                            catch (Exception E)
                            {
                                LogException(E);
                            }

                        }
                    }
                }
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (CM != null) LogMethodEnd(CM);
            }


        }

        private async Task<bool> GetSettingTicketAndServiceRequestEnabledAsync(CancellationToken Token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }

            await _GlobalSettingsQueryCriticalSection.Semaphore.WaitAsync();
            try
            {
                await CheckAndRefreshGlobalServerSettingsAsync(Token);
                return _globalServerSettings.TicketAndServiceRequestEnabled;
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                _GlobalSettingsQueryCriticalSection.Semaphore.Release();
                if (CM != null) LogMethodEnd(CM);
            }

            return true;
        }

        public override async Task<bool> DoScanAsync(bool Always, bool Rescan, cF4sdWebRequestInfo requestInfo, int LogDeep, CancellationToken Token, bool EnhancedDebug = false)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); }
            var _startTime = DateTime.UtcNow;

            try
            {
                if (Collector.M42WpmCollector is null)
                    return false;

                Collector.DoProcessUiMessage(0, "");
                if (!Always)
                {
                    var scanInfo = await Collector.M42WpmCollector.GetScanTimeInfoAsync(requestInfo, LogDeep + 1, Token);
                    if (scanInfo?.LastScan is DateTime _lastScan && scanInfo?.NextScan is DateTime _nextScan)
                    {
                        if (_lastScan >= _nextScan)
                        {
                            Collector.DoProcessUiMessage(0, $"currently no scan needed for M42 api token cleanup, lastScan={_lastScan} (UTC), nextScan={_nextScan} (UTC)");
                            return false;
                        }
                    }
                }

                await CleanupApiTokensAsync(Token);
                if (Token.IsCancellationRequested)
                    return false;

                await Collector.SetLastScanTime("M42Scan-CleanupToken", DateTime.UtcNow, requestInfo, LogDeep + 1, Token);

                return true;
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }

            return true;
        }

        public override async Task<cScanTimeInfo> GetScanTimeInfoAsync(cF4sdWebRequestInfo requestInfo, int LogDeep, CancellationToken Token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); }
            var _startTime = DateTime.UtcNow;

            try
            {
                var scanTiming = new cDataHistoryScanTiming()
                {
                    IsValid = true,
                    Interval = TimeSpan.FromDays(1),
                    Offset = TimeSpan.FromHours(1),
                    Timezone = TimeZoneInfo.Local
                };
                return await GetScanTimeInfoAsync(scanTiming, "M42Scan-CleanupToken", requestInfo, LogDeep + 1, Token);
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }

            return null;

        }

        private async Task<bool> CheckLogonAsync(bool force, CancellationToken Token)
        {
            if (!force && WebClient.BearerToken != null)
                return true;

            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            try
            {
                if (Collector.InfrastructureConfig.M42Wpm == null)
                    return false;

                Collector.DoProcessUiMessage(0, "");
                Collector.DoProcessUiMessage(0, $"Start estabishing M42 WPM connection to server {Collector.InfrastructureConfig.M42Wpm.Server}...");
                var resLogon = await WebClient.LogonWithApiKeyAsync(Collector.InfrastructureConfig.M42Wpm.Credential.NwCredential.Password);
                if (resLogon == cM42WebClient.eReason.ok)
                {
                    Collector.DoProcessUiMessage(0, "...M42 WPM connection established successful.");
                    StartOnlineValidation();
                    return true;
                }

                Collector.DoProcessUiMessage(1, $"Logon failed: {resLogon}");
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                Collector.DoProcessUiMessage(0, "");
                if (CM != null) LogMethodEnd(CM);
            }

            Collector.DoProcessUiMessage(0, "Error: Could not connect to M42 WPM.");
            return false;
        }

        public async Task<bool> ValidateConnectionAsync(bool forceLogon, CancellationToken Token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }

            try
            {
                await _OnlineCheckCriticalSection.Semaphore.WaitAsync(Token);
                try
                {
                    var CheckResult = true;
                    if (!IsOnline || forceLogon)
                        CheckResult = await CheckLogonAsync(true, Token);
                    if (CheckResult)
                    {
                        var resWeb = await WebClient.HttpEnh.GetAsync(constUrlIsAlive, Token);
                        CheckResult = resWeb != null && resWeb.IsSuccessStatusCode;
                        if (forceLogon)
                        {
                            if (CheckResult)
                                Collector.DoProcessUiMessage(0, "...F4SD web api 'is alive' is reachable.");
                            else
                                Collector.DoProcessUiMessage(0, "Error: F4SD web api 'is alive' is not reachable. Maybe F4SD support is not installed or wrong version.", LogLevels.Error);
                        }
                    }

                    IsOnline = CheckResult;

                    if (onlineTimer != null)
                        onlineTimer.Change(30000, 60000);

                }
                catch (Exception E)
                {
                    LogException(E);
                }
                finally
                {
                    _OnlineCheckCriticalSection.Semaphore.Release();
                }

                if (IsOnline)
                {
                    await _GlobalSettingsQueryCriticalSection.Semaphore.WaitAsync(Token);
                    try
                    {
                        await CheckAndRefreshGlobalServerSettingsAsync(Token);
                    }
                    catch (Exception E)
                    {
                        LogException(E);
                    }
                    finally
                    {
                        _GlobalSettingsQueryCriticalSection.Semaphore.Release();
                    }
                }

                return IsOnline;
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (CM != null) LogMethodEnd(CM);
            }

            return false;
        }

        private enumTicketStatus ConvertM42TicketCode(int M42TicketStatus)
        {
            try
            {
                switch (M42TicketStatus)
                {
                    case 200:
                        return enumTicketStatus.New;
                    case 201:
                    case 202:
                        return enumTicketStatus.InProgress;
                    case 203:
                        return enumTicketStatus.OnHold;
                    case 204:
                        return enumTicketStatus.Closed;
                }
            }
            catch (Exception E)
            {
                LogException(E);
            }
            return enumTicketStatus.Unknown;
        }

        public async Task<List<cF4SDTicketSummary>> GetTicketsFromSid(cF4sdConnectorIds UserId, CancellationToken Token, cF4sdWebRequestInfo requestInfo)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            try
            {
                if (!await CheckOnline())
                    return null;

                var objSid = new SecurityIdentifier(UserId.sid, 0);
                var strSid = objSid.ToString();
                var queueOption = (int)Collector.InfrastructureConfig.M42Wpm.ActivityQueueFilterOption;
                var strUrl = string.Format(constUrlGetTickets,
                    strSid,
                    Collector.InfrastructureConfig.M42Wpm.ClosedTicketHistory,
                    queueOption);

                if (queueOption != 0)
                {
                    var encodedQueues = GetEncodedActivityQueues();
                    if (!string.IsNullOrWhiteSpace(encodedQueues))
                        strUrl += string.Format(constUrlGetTicketsQueuesParam, encodedQueues);
                }

                var _wc = await GetWebClient(requestInfo, Token);
                var res = await _wc.HttpEnh.GetAsync(strUrl);
                if (res.IsSuccessStatusCode)
                {
                    var strJson = await res.Content.ReadAsStringAsync();
                    if (!string.IsNullOrEmpty(strJson))
                    {
                        var ticketInfoList = JsonConvert.DeserializeObject<List<cF4SDTicketSummary>>(strJson);
                        if (!(ticketInfoList?.Count > 0))
                            return null;

                        foreach (var Entry in ticketInfoList)
                            Entry.UserId = UserId.Id;

                        return ticketInfoList;
                    }
                }

                StartOnlineValidation();
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (CM != null) LogMethodEnd(CM);
            }

            return null;
        }

        public async Task<List<cF4sdApiSearchResultRelation>> getTicketUsage(IEnumerable<cF4sdConnectorIds> lstTicketIds, CancellationToken Token, cF4sdWebRequestInfo requestInfo, int LogDeep)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); }
            var _startTime = DateTime.UtcNow;

            var _ticketAndServiceRequestEnabled = await GetSettingTicketAndServiceRequestEnabledAsync(Token);

            try
            {
                var lstTasks = new List<Task<List<cF4SDTicketSummary>>>();
                foreach (var Entry in lstTicketIds)
                    lstTasks.Add(GetTicketsFromSid(Entry, Token, requestInfo));

                var result = await Task.WhenAll(lstTasks);

                if (result == null)
                    return null;

                var retVal = new Dictionary<string, cF4SDTicketSummary>();
                foreach (var Entry in result)
                    if (Entry != null)
                    {
                        foreach (var Entry2 in Entry)
                        {
                            if (Entry2.IsPrimaryAccount)
                                retVal[Entry2.Name] = Entry2;
                            else if (!retVal.ContainsKey(Entry2.Name))
                                retVal.Add(Entry2.Name, Entry2);
                        }
                    }

                if (retVal.Count > 0)
                {
                    var ticketResult = new List<cF4sdApiSearchResultRelation>(retVal.Count);
                    foreach (var Entry in retVal.Values)
                    {
                        var TicketType = ConvertActivityToTicketType(Entry.ActivityType, _ticketAndServiceRequestEnabled);
                        var DeepLink = BuildTicketDeepLink(Entry.TicketObjectId, Entry.ActivityType);
                        var Infos = new Dictionary<string, string>()
                            {
                                { "Summary", Entry.Summary },
                                { "TicketType", TicketType.ToString() },
                                { "ActivityType", Entry.ActivityType },
                                { "Status", Entry.Status },
                                { "StatusId", ConvertM42TicketCode(Entry.StatusId).ToString() },
                                { "Asset", Entry.AssetName },
                                { "ServiceName", Entry.ServiceName },
                                { "ServiceId", Entry.ServiceId.ToString() },
                                { "AssetType", Entry.AssetCIName },
                                { "TicketLink", DeepLink  }
                            };

                        var ticketInfo = new cF4sdApiSearchResultRelation()
                        {
                            Type = enumF4sdSearchResultClass.Ticket,
                            Name = Entry.Name,
                            DisplayName = Entry.Name,
                            id = Entry.TicketObjectId,
                            Status = enumF4sdSearchResultStatus.Active,
                            Infos = Infos,
                            Identities = new cF4sdIdentityList()
                            {
                                new cF4sdIdentityEntry(){ Class = enumFasdInformationClass.User, Id = Entry.UserId},
                                new cF4sdIdentityEntry(){ Class = enumFasdInformationClass.Ticket, Id = Entry.TicketObjectId}
                            }
                        };
                        ticketResult.Add(ticketInfo);
                    }

                    return ticketResult;
                }
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }

            return null;
        }

        private TimeSpan GetTicketOverviewCacheDuration(bool useRoleScope)
        {
            var ticketConfig = Collector?.GetGlobalConfig()?.TicketConfiguration;
            var minutes = useRoleScope
                ? ticketConfig?.OverviewPollingRole ?? cF4sdTicketConfig.DefaultOverviewPollingRole
                : ticketConfig?.OverviewPollingPersonal ?? cF4sdTicketConfig.DefaultOverviewPollingPersonal;

            if (minutes < 1)
                minutes = 1;

            return TimeSpan.FromMinutes(minutes);
        }

        private bool TryGetTicketOverviewPersonalCache(string sid, DateTime now, out Dictionary<string, int> counts)
        {
            counts = null;
            if (string.IsNullOrWhiteSpace(sid))
                return false;

            lock (_ticketOverviewCacheLock)
            {
                if (_ticketOverviewPersonalCache.TryGetValue(sid, out var entry))
                {
                    if (entry.ExpiresAtUtc > now)
                    {
                        counts = new Dictionary<string, int>(entry.Counts, StringComparer.OrdinalIgnoreCase);
                        return true;
                    }

                    _ticketOverviewPersonalCache.Remove(sid);
                }
            }

            return false;
        }

        private bool TryGetTicketOverviewRoleCache(Guid roleId, DateTime now, out Dictionary<string, int> counts)
        {
            counts = null;
            if (roleId == Guid.Empty)
                return false;

            lock (_ticketOverviewCacheLock)
            {
                if (_ticketOverviewRoleCache.TryGetValue(roleId, out var entry))
                {
                    if (entry.ExpiresAtUtc > now)
                    {
                        counts = new Dictionary<string, int>(entry.Counts, StringComparer.OrdinalIgnoreCase);
                        return true;
                    }

                    _ticketOverviewRoleCache.Remove(roleId);
                }
            }

            return false;
        }

        private void SetTicketOverviewPersonalCache(string sid, Dictionary<string, int> counts, DateTime expiresAtUtc)
        {
            if (string.IsNullOrWhiteSpace(sid))
                return;

            lock (_ticketOverviewCacheLock)
            {
                _ticketOverviewPersonalCache[sid] = new TicketOverviewCountCacheEntry
                {
                    Counts = new Dictionary<string, int>(counts ?? new Dictionary<string, int>(), StringComparer.OrdinalIgnoreCase),
                    ExpiresAtUtc = expiresAtUtc
                };
            }
        }

        private void SetTicketOverviewRoleCache(Guid roleId, Dictionary<string, int> counts, DateTime expiresAtUtc)
        {
            if (roleId == Guid.Empty)
                return;

            lock (_ticketOverviewCacheLock)
            {
                _ticketOverviewRoleCache[roleId] = new TicketOverviewCountCacheEntry
                {
                    Counts = new Dictionary<string, int>(counts ?? new Dictionary<string, int>(), StringComparer.OrdinalIgnoreCase),
                    ExpiresAtUtc = expiresAtUtc
                };
            }
        }

        private bool TryGetTicketOverviewRoleIdsFromCache(string sid, DateTime now, out List<Guid> roleIds)
        {
            roleIds = null;
            if (string.IsNullOrWhiteSpace(sid))
                return false;

            lock (_ticketOverviewCacheLock)
            {
                if (_ticketOverviewRoleListCache.TryGetValue(sid, out var entry))
                {
                    if (entry.ExpiresAtUtc > now)
                    {
                        roleIds = new List<Guid>(entry.RoleIds);
                        return true;
                    }

                    _ticketOverviewRoleListCache.Remove(sid);
                }
            }

            return false;
        }

        private void SetTicketOverviewRoleIdsCache(string sid, IEnumerable<Guid> roleIds, DateTime expiresAtUtc)
        {
            if (string.IsNullOrWhiteSpace(sid))
                return;

            var normalized = (roleIds ?? Enumerable.Empty<Guid>())
                .Where(id => id != Guid.Empty)
                .Distinct()
                .ToList();

            lock (_ticketOverviewCacheLock)
            {
                _ticketOverviewRoleListCache[sid] = new TicketOverviewRoleListCacheEntry
                {
                    RoleIds = normalized,
                    ExpiresAtUtc = expiresAtUtc
                };
            }
        }

        private async Task<List<Guid>> GetTicketOverviewRoleIdsAsync(string sid, DateTime now, TimeSpan ttl, CancellationToken token)
        {
            if (TryGetTicketOverviewRoleIdsFromCache(sid, now, out var cached))
                return cached;

            var userInfo = await GetM42UserInfoAsync(sid, token);
            var roles = userInfo?.Roles ?? new List<M42UserUserInfo.M42Role>();
            var roleIds = roles
                .Where(r => r != null && r.Id != Guid.Empty)
                .Select(r => r.Id)
                .Distinct()
                .ToList();

            SetTicketOverviewRoleIdsCache(sid, roleIds, now.Add(ttl));
            return roleIds;
        }

        private async Task<Dictionary<Guid, Dictionary<string, int>>> FetchTicketOverviewCountsByRolesAsync(
            IEnumerable<Guid> roleIds,
            IReadOnlyCollection<string> requestedKeys,
            cF4sdWebRequestInfo requestInfo,
            int logDeep,
            CancellationToken token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(logDeep, CM, requestInfo.id, requestInfo.created); }
            var startTime = DateTime.UtcNow;

            try
            {
                var roleList = (roleIds ?? Enumerable.Empty<Guid>())
                    .Where(id => id != Guid.Empty)
                    .Distinct()
                    .ToList();

                if (roleList.Count == 0)
                    return new Dictionary<Guid, Dictionary<string, int>>();

                var queueOption = GetActivityQueueOption();
                var encodedQueues = queueOption != 0 ? GetEncodedActivityQueues() : null;

                var request = new TicketOverviewCountsByRolesRequest
                {
                    Sid = requestInfo?.userInfo?.AdSid,
                    RoleGuids = roleList,
                    Keys = requestedKeys?.Where(k => !string.IsNullOrWhiteSpace(k)).ToList() ?? new List<string>(),
                    QueueOption = queueOption,
                    Queues = encodedQueues
                };

                var jsonBody = JsonConvert.SerializeObject(request, Formatting.None,
                    new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
                var content = new StringContent(jsonBody, Encoding.UTF8, "application/json");

                var wc = await GetWebClient(requestInfo, token);
                var res = await wc.HttpEnh.PostAsync(constUrlGetTicketOverviewCountsByRoles, content, token);
                if (token.IsCancellationRequested)
                    return new Dictionary<Guid, Dictionary<string, int>>();

                if (res?.IsSuccessStatusCode == true)
                {
                    var json = await res.Content.ReadAsStringAsync();
                    if (!string.IsNullOrWhiteSpace(json))
                    {
                        var response = JsonConvert.DeserializeObject<TicketOverviewCountsByRolesResponse>(json);
                        if (response?.CountsByRole != null)
                        {
                            var result = new Dictionary<Guid, Dictionary<string, int>>();
                            foreach (var entry in response.CountsByRole)
                            {
                                if (!Guid.TryParse(entry.Key, out var roleId) || roleId == Guid.Empty)
                                    continue;

                                var counts = entry.Value ?? new Dictionary<string, int>();
                                result[roleId] = new Dictionary<string, int>(counts, StringComparer.OrdinalIgnoreCase);
                            }
                            return result;
                        }
                    }
                }

                StartOnlineValidation();
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(logDeep, CM, requestInfo.id, requestInfo.created, startTime); }
                if (CM != null) LogMethodEnd(CM);
            }

            return new Dictionary<Guid, Dictionary<string, int>>();
        }

        private Dictionary<string, int> SumTicketOverviewCounts(IEnumerable<Dictionary<string, int>> sources)
        {
            var result = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
            if (sources == null)
                return result;

            foreach (var counts in sources)
            {
                if (counts == null)
                    continue;

                foreach (var kvp in counts)
                {
                    if (string.IsNullOrWhiteSpace(kvp.Key))
                        continue;

                    result[kvp.Key] = result.TryGetValue(kvp.Key, out var existing)
                        ? existing + kvp.Value
                        : kvp.Value;
                }
            }

            return result;
        }

        private Dictionary<string, int> FilterTicketOverviewCounts(Dictionary<string, int> counts, IReadOnlyCollection<string> requestedKeys)
        {
            if (counts == null)
                return new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);

            if (requestedKeys == null || requestedKeys.Count == 0)
                return new Dictionary<string, int>(counts, StringComparer.OrdinalIgnoreCase);

            var filtered = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
            foreach (var key in requestedKeys)
            {
                if (string.IsNullOrWhiteSpace(key))
                    continue;

                filtered[key] = counts.TryGetValue(key, out var value) ? value : 0;
            }

            return filtered;
        }

        private int GetActivityQueueOption()
        {
            return (int)Collector.InfrastructureConfig.M42Wpm.ActivityQueueFilterOption;
        }

        private string GetEncodedActivityQueues()
        {
            var queues = Collector.InfrastructureConfig.M42Wpm.ActivityQueues;
            if (queues == null || queues.Count == 0)
                return null;

            var encodedQueues = queues
                .Where(q => q != null)
                .Select(q =>
                {
                    if (q.QueueID != Guid.Empty)
                    {
                        // If both are configured, prefer ID-based filtering and ignore the name.
                        return ":" + HttpUtility.UrlEncode(q.QueueID.ToString());
                    }

                    if (!string.IsNullOrWhiteSpace(q.QueueName))
                        return HttpUtility.UrlEncode(q.QueueName.Trim());

                    return null;
                })
                .Where(entry => !string.IsNullOrWhiteSpace(entry))
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .ToList();

            return encodedQueues.Count > 0 ? string.Join("|", encodedQueues) : null;
        }

        private static void AppendQueueFilterQuery(StringBuilder urlBuilder, int queueOption, string encodedQueues)
        {
            if (queueOption != 0)
                urlBuilder.Append("&queueoption=").Append(queueOption);

            if (!string.IsNullOrWhiteSpace(encodedQueues))
                urlBuilder.Append("&queues=").Append(encodedQueues);
        }

        private async Task<Dictionary<string, int>> FetchTicketOverviewCountsAsync(
            string sid,
            string scope,
            IReadOnlyCollection<string> requestedKeys,
            cF4sdWebRequestInfo requestInfo,
            CancellationToken token)
        {
            try
            {
                var urlBuilder = new StringBuilder(constUrlGetTicketOverviewCounts);
                urlBuilder.Append("?sid=").Append(HttpUtility.UrlEncode(sid));
                urlBuilder.Append("&scope=").Append(HttpUtility.UrlEncode(scope ?? string.Empty));

                if (requestedKeys != null && requestedKeys.Count > 0)
                {
                    urlBuilder.Append("&keys=").Append(HttpUtility.UrlEncode(string.Join(",", requestedKeys)));
                }

                var queueOption = GetActivityQueueOption();
                var encodedQueues = queueOption != 0 ? GetEncodedActivityQueues() : null;
                AppendQueueFilterQuery(urlBuilder, queueOption, encodedQueues);

                var url = urlBuilder.ToString();
                var wc = await GetWebClient(requestInfo, token);
                var res = await wc.HttpEnh.GetAsync(url, token);
                if (token.IsCancellationRequested)
                    return new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);

                if (res?.IsSuccessStatusCode == true)
                {
                    var json = await res.Content.ReadAsStringAsync();
                    if (!string.IsNullOrWhiteSpace(json))
                    {
                        var root = JsonConvert.DeserializeObject<JObject>(json);
                        var ticketCapability = root?["capabilities"]?["ticketAndServiceRequestsEnabled"];
                        if (ticketCapability != null && ticketCapability.Type != JTokenType.Null)
                        {
                            if (ticketCapability.Type == JTokenType.Boolean)
                            {
                                _globalServerSettings.TicketAndServiceRequestEnabled = ticketCapability.Value<bool>();
                                _globalServerSettings.LastRead = DateTime.UtcNow;
                            }
                            else if (bool.TryParse(ticketCapability.ToString(), out var isEnabled))
                            {
                                _globalServerSettings.TicketAndServiceRequestEnabled = isEnabled;
                                _globalServerSettings.LastRead = DateTime.UtcNow;
                            }
                        }

                        var countsToken = root?["counts"] as JObject;
                        var counts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
                        if (countsToken != null)
                        {
                            foreach (var prop in countsToken.Properties())
                            {
                                if (prop?.Name == null)
                                    continue;
                                var value = prop.Value?.Type == JTokenType.Integer
                                    ? prop.Value.Value<int>()
                                    : int.TryParse(prop.Value?.ToString(), out var parsed) ? parsed : 0;
                                counts[prop.Name] = value;
                            }
                        }
                        return counts;
                    }
                }

                StartOnlineValidation();
            }
            catch (Exception E)
            {
                LogException(E);
            }

            return new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
        }

        public async Task<Dictionary<string, int>> GetTicketOverviewCountsAsync(IEnumerable<string> keys, bool useRoleScope, cF4sdWebRequestInfo requestInfo, int LogDeep, CancellationToken Token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); }
            var _startTime = DateTime.UtcNow;

            try
            {
                if (!await CheckOnline())
                    return new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);

                var sid = requestInfo?.userInfo?.AdSid;
                if (string.IsNullOrWhiteSpace(sid))
                    return new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);

                var normalizedKeys = (keys ?? Enumerable.Empty<string>())
                    .Where(k => !string.IsNullOrWhiteSpace(k))
                    .Distinct(StringComparer.OrdinalIgnoreCase)
                    .ToList();
                var now = DateTime.UtcNow;
                var ttl = GetTicketOverviewCacheDuration(useRoleScope);

                if (!useRoleScope)
                {
                    // process thr personal scope
                    if (TryGetTicketOverviewPersonalCache(sid, now, out var cachedPersonal))
                        return FilterTicketOverviewCounts(cachedPersonal, normalizedKeys);

                    var counts = await FetchTicketOverviewCountsAsync(sid, "personal", normalizedKeys, requestInfo, Token);
                    if (Token.IsCancellationRequested)
                        return new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);

                    SetTicketOverviewPersonalCache(sid, counts, now.Add(ttl));
                    return FilterTicketOverviewCounts(counts, normalizedKeys);
                }

                // process the role scope
                var roleIds = await GetTicketOverviewRoleIdsAsync(sid, now, ttl, Token);
                if (roleIds == null || roleIds.Count == 0)
                    return new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);

                var countsByRole = new Dictionary<Guid, Dictionary<string, int>>();
                var missingRoles = new List<Guid>();

                foreach (var roleId in roleIds)
                {
                    if (TryGetTicketOverviewRoleCache(roleId, now, out var cachedRole))
                    {
                        countsByRole[roleId] = cachedRole;
                    }
                    else
                    {
                        missingRoles.Add(roleId);
                    }
                }

                if (missingRoles.Count > 0)
                {
                    var fetched = await FetchTicketOverviewCountsByRolesAsync(missingRoles, normalizedKeys, requestInfo, LogDeep + 1, Token);
                    var expiresAt = DateTime.UtcNow.Add(ttl);

                    foreach (var roleId in missingRoles)
                    {
                        if (!fetched.TryGetValue(roleId, out var roleCounts))
                            roleCounts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);

                        SetTicketOverviewRoleCache(roleId, roleCounts, expiresAt);
                        countsByRole[roleId] = roleCounts;
                    }
                }

                var summed = SumTicketOverviewCounts(countsByRole.Values);
                return FilterTicketOverviewCounts(summed, normalizedKeys);
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }

            return new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
        }

        public async Task<List<cF4sdApiSearchResultRelation>> GetTicketOverviewRelationsAsync(string key, bool useRoleScope, int count, cF4sdWebRequestInfo requestInfo, int LogDeep, CancellationToken Token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); }
            var _startTime = DateTime.UtcNow;

            try
            {
                if (!await CheckOnline())
                    return new List<cF4sdApiSearchResultRelation>();

                if (string.IsNullOrWhiteSpace(key))
                    return new List<cF4sdApiSearchResultRelation>();

                var sid = requestInfo?.userInfo?.AdSid;
                if (string.IsNullOrWhiteSpace(sid))
                    return new List<cF4sdApiSearchResultRelation>();

                var scope = useRoleScope ? "role" : "personal";
                var urlBuilder = new StringBuilder(constUrlGetTicketOverviewRelations);
                urlBuilder.Append("?sid=").Append(HttpUtility.UrlEncode(sid));
                urlBuilder.Append("&scope=").Append(HttpUtility.UrlEncode(scope));
                urlBuilder.Append("&key=").Append(HttpUtility.UrlEncode(key));
                urlBuilder.Append("&count=").Append(Math.Max(0, count));

                var queueOption = GetActivityQueueOption();
                var encodedQueues = queueOption != 0 ? GetEncodedActivityQueues() : null;
                AppendQueueFilterQuery(urlBuilder, queueOption, encodedQueues);

                var url = urlBuilder.ToString();

                var wc = await GetWebClient(requestInfo, Token);
                var res = await wc.HttpEnh.GetAsync(url, Token);
                if (Token.IsCancellationRequested)
                    return new List<cF4sdApiSearchResultRelation>();

                if (res?.IsSuccessStatusCode == true)
                {
                    var json = await res.Content.ReadAsStringAsync();
                    if (!string.IsNullOrWhiteSpace(json))
                    {
                        var relations = JsonConvert.DeserializeObject<List<cF4sdApiSearchResultRelation>>(json);
                        if (relations != null)
                            return relations;
                    }
                }

                StartOnlineValidation();
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }

            return new List<cF4sdApiSearchResultRelation>();
        }

        public override async Task<List<cF4SDHealthCardRawData.cHealthCardTable>> GetTableResultsVirtualAsync(List<cDataHistoryConfigTable> Tables, Dictionary<enumFasdInformationClass, cF4sdConnectorIds> Identities, DateTime RefTime, int MaxAge, bool instantly, Guid? CacheId, CancellationToken Token, cF4sdWebRequestInfo requestInfo, int LogDeep)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); }
            var _startTime = DateTime.UtcNow;

            try
            {
                if (!await CheckOnline())
                    return null;

                var bMainExists = Tables.Exists(v => v.Name == constTableNameTicketDetails && v.ParentCluster?.Origin == enumDataHistoryOrigin.M42Wpm);
                var bHistoryExists = Tables.Exists(v => v.Name == constTableNameTicketHistory && v.ParentCluster?.Origin == enumDataHistoryOrigin.M42Wpm);
                var bUserInfoExists = Tables.Exists(v => v.Name == constTableNameUserTicket && v.ParentCluster?.Origin == enumDataHistoryOrigin.M42Wpm);
                var bPickupsExists = Tables.Exists(v => v.ParentCluster?.Origin == enumDataHistoryOrigin.M42Wpm && v.SubTables?.Count == 1 && v.SubTables.Values.First().Template is cDataHistoryConfigQueryTemplateM42Pickup);

                if (!bMainExists && !bHistoryExists && !bUserInfoExists && !bPickupsExists)
                    return null;


                if (!Identities.TryGetValue(enumFasdInformationClass.Ticket, out var idTicket))
                {
                    bMainExists = false;
                    bHistoryExists = false;
                }

                if (!bMainExists && !bHistoryExists && !bUserInfoExists && !bPickupsExists)
                    return null;

                var listTasks = new List<Task<List<cF4SDHealthCardRawData.cHealthCardTable>>>(2);
                if (bMainExists && CacheId == null)
                    listTasks.Add(Task.Run<List<cF4SDHealthCardRawData.cHealthCardTable>>(async () => { return await GetTicketMainTableAsync(idTicket.Id, requestInfo, LogDeep + 1, Token); }));
                if (bHistoryExists && (instantly || CacheId != null))
                    listTasks.Add(Task.Run<List<cF4SDHealthCardRawData.cHealthCardTable>>(async () => { return await GetTicketHistoryTableAsync(idTicket.Id, requestInfo, LogDeep + 1, Token); }));
                if (bUserInfoExists && (CacheId == null))
                    listTasks.Add(Task.Run<List<cF4SDHealthCardRawData.cHealthCardTable>>(async () => { return await GetUserTicketAsync(Identities, Token, requestInfo, LogDeep + 1); }));
                if (bPickupsExists && (instantly || CacheId != null))
                {
                    foreach (var _table in Tables)
                    {
                        if (_table.ParentCluster?.Origin != enumDataHistoryOrigin.M42Wpm)
                            continue;
                        if (!(_table.SubTables.Values.First()?.Template is cDataHistoryConfigQueryTemplateM42Pickup))
                            continue;

                        listTasks.Add(Task.Run<List<cF4SDHealthCardRawData.cHealthCardTable>>(async () => { return await GetUserPickupValuesAsync(_table, requestInfo, LogDeep + 1, Token); }));
                    }
                }

                var arrRes = await Task.WhenAll(listTasks.ToArray());
                var retVal = new List<cF4SDHealthCardRawData.cHealthCardTable>();
                foreach (var Entry in arrRes)
                    if (Entry != null)
                        retVal.AddRange(Entry);


                // add the incomplete tables
                if (CacheId == null)
                {
                    if (bHistoryExists)
                        retVal.Add(new cF4SDHealthCardRawData.cHealthCardTable()
                        {
                            Name = constTableNameTicketHistory,
                            InformationClass = enumFasdInformationClass.Ticket,
                            Origin = enumDataHistoryOrigin.M42Wpm,
                            IsIncomplete = true
                        });

                    foreach (var _table in Tables)
                    {
                        if (_table.ParentCluster?.Origin != enumDataHistoryOrigin.M42Wpm)
                            continue;
                        if (!(_table.SubTables.Values.First()?.Template is cDataHistoryConfigQueryTemplateM42Pickup))
                            continue;

                        retVal.Add(new cF4SDHealthCardRawData.cHealthCardTable()
                        {
                            Name = _table.Name,
                            InformationClass = _table.ParentCluster.InformationClass,
                            Origin = _table.ParentCluster.Origin,
                            IsIncomplete = true
                        });
                    }
                }


                if (retVal.Count == 0)
                    return null;

                return retVal;

            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }

            return null;
        }

        private async Task<List<cF4SDHealthCardRawData.cHealthCardTable>> GetTicketMainTableAsync(Guid TicketId, cF4sdWebRequestInfo requestInfo, int LogDeep, CancellationToken Token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); }
            var _startTime = DateTime.UtcNow;

            try
            {
                if (!Collector.ClusterConfig.Tables.TryGetValue(constTableNameTicketDetails, out var M42TableDetails))
                    return null;

                var strUrl = string.Format(constUrlGetTicketDetails, TicketId);
                var _wc = await GetWebClient(requestInfo, Token);
                var res = await _wc.HttpEnh.GetAsync(strUrl);

                if (cLogManager.DefaultLogger.IsDebug) LogEntry("SpecialDebug GetTicketMainTableAsync #1");
                if (res?.IsSuccessStatusCode == true)
                {
                    var strJson = await res.Content.ReadAsStringAsync();

                    if (!string.IsNullOrEmpty(strJson))
                    {
                        if (cLogManager.DefaultLogger.IsDebug) LogEntry("SpecialDebug GetTicketMainTableAsync #2");

                        JObject ticketInfos = JsonConvert.DeserializeObject<JObject>(strJson);
                        var retVal = new cF4SDHealthCardRawData.cHealthCardTable()
                        {
                            Name = constTableNameTicketDetails,
                            InformationClass = enumFasdInformationClass.Ticket,
                            Origin = enumDataHistoryOrigin.M42Wpm,
                            IsStatic = true,
                            TableType = eDataHistoryTableType.Static
                        };

                        var dicVals = new Dictionary<string, object>();
                        var vals = ticketInfos.Children();

                        if (cLogManager.DefaultLogger.IsDebug) LogEntry("SpecialDebug GetTicketMainTableAsync #3");

                        foreach (var token in vals)
                        {
                            try
                            {
                                if (token is JProperty prop)
                                    if (prop.Value is JValue val)
                                        dicVals[prop.Name] = val.Value;
                            }
                            catch (Exception E)
                            {
                                LogEntry($"Error getting M42 ticket detail property '{token.Path}'", LogLevels.Error);
                                LogException(E);
                            }

                        }

                        if (cLogManager.DefaultLogger.IsDebug) LogEntry("SpecialDebug GetTicketMainTableAsync #4");

                        foreach (var colInfo in M42TableDetails.Columns.Values)
                        {
                            try
                            {
                                if (!dicVals.TryGetValue(colInfo.SourceName, out var objVal))
                                    objVal = null;

                                objVal = ConvertToF4sdType(objVal, colInfo.ValueType);

                                var _col = new cF4SDHealthCardRawData.cHealthCardTableColumn(retVal) { ColumnName = colInfo.Name, Values = new List<object>(1) { objVal } };
                                if (colInfo.IsWritable && colInfo.SourceName == "SolutionHtml")
                                    _col.IsWritable = true;

                                retVal.Columns[colInfo.Name] = _col;
                            }
                            catch { }
                        }

                        if (cLogManager.DefaultLogger.IsDebug) LogEntry("SpecialDebug GetTicketMainTableAsync #5");

                        return new List<cF4SDHealthCardRawData.cHealthCardTable>(1) { retVal };
                    }
                    else
                    {
                        LogEntry($"Could not get detailed ticket information for ticket wit id {TicketId}. Ticket content is empty.", LogLevels.Warning);
                    }
                }
                else
                {
                    cLogManager.DefaultLogger.LogHttpResponseMessage(res, $"Could not get detailed ticket information for ticket wit id {TicketId}.", LogLevels.Warning);
                }

                if (cLogManager.DefaultLogger.IsDebug) LogEntry("SpecialDebug GetTicketMainTableAsync #6");

                StartOnlineValidation();

                if (cLogManager.DefaultLogger.IsDebug) LogEntry("SpecialDebug GetTicketMainTableAsync #7");
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }

            return null;
        }

        public async Task<cTicketExternalLinkResult> GetTicketExternalLinkAsync(Guid ticketId, enumTicketExternalOpenMode mode, cF4sdWebRequestInfo requestInfo, int logDeep, CancellationToken token)
        {
            var result = new cTicketExternalLinkResult
            {
                Success = false,
                Mode = enumTicketExternalOpenMode.None
            };

            if (ticketId == Guid.Empty || mode == enumTicketExternalOpenMode.None)
                return result;

            var ticketDetails = await GetTicketMainTableAsync(ticketId, requestInfo, logDeep + 1, token);
            var ticketTable = ticketDetails?.FirstOrDefault();
            if (ticketTable?.Columns == null)
                return result;

            string previewUrl = null;
            if (ticketTable.Columns.TryGetValue("DirectLinkPreview", out var previewColumn))
                previewUrl = previewColumn.Values?.FirstOrDefault()?.ToString();

            var resolvedMode = mode;
            var url = previewUrl;
            if (mode == enumTicketExternalOpenMode.Edit)
            {
                string editUrl = null;
                if (ticketTable.Columns.TryGetValue("DirectLinkEdit", out var editColumn))
                    editUrl = editColumn.Values?.FirstOrDefault()?.ToString();

                if (!string.IsNullOrWhiteSpace(editUrl))
                    url = editUrl;
                else
                    resolvedMode = enumTicketExternalOpenMode.Preview;
            }

            if (string.IsNullOrWhiteSpace(url))
                return result;

            result.Success = true;
            result.Url = url;
            result.Mode = resolvedMode;
            return result;
        }

        private object ConvertToF4sdType(object objVal, enumFasdValueType ValueType)
        {
            try
            {
                if (objVal != null)
                {
                    switch (ValueType)
                    {
                        case enumFasdValueType.INT:
                            return cF4SDHealthCardRawData.GetInteger(objVal);
                        case enumFasdValueType.GUID:
                            return cXmlParser.GetGuidFromString(objVal.ToString());
                        case enumFasdValueType.BOOLEAN:

                            if (objVal == null) return false;
                            if (objVal is bool) return (bool)objVal;
                            if (objVal is int) return (int)objVal != 0;
                            if (objVal is string) return bool.TryParse((string)objVal, out bool result) ? result : ((string)objVal == "1");
                            return false; // Standardrückgabewert, falls keine Bedingung zutrifft
                        case enumFasdValueType.DATETIME:
                            DateTime? dt = null;
                            if (objVal is DateTime _dt)
                                dt = _dt;
                            else
                            {
                                if (DateTime.TryParse(objVal.ToString(), out _dt))
                                    dt = _dt;
                            }
                            if (dt is DateTime _dt2)
                            {
                                if (_dt2 == new DateTime(1, 1, 1, 0, 0, 0))
                                    dt = null;
                            }
                            return dt;
                        default:
                            return objVal.ToString();
                    }
                }
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
            }

            return null;
        }

        private async Task<List<cF4SDHealthCardRawData.cHealthCardTable>> GetTicketHistoryTableAsync(Guid TicketId, cF4sdWebRequestInfo requestInfo, int LogDeep, CancellationToken Token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); }
            var _startTime = DateTime.UtcNow;

            try
            {
                if (!Collector.ClusterConfig.Tables.TryGetValue(constTableNameTicketHistory, out var M42TableDetails))
                    return null;

                var strUrl = string.Format(constUrlGetTicketHistory, TicketId);
                var _wc = await GetWebClient(requestInfo, Token);
                var res = await _wc.HttpEnh.GetAsync(strUrl);

                if (res?.IsSuccessStatusCode == true)
                {
                    var strJson = await res.Content.ReadAsStringAsync();

                    if (!string.IsNullOrEmpty(strJson))
                    {
                        JContainer ticketInfos = JsonConvert.DeserializeObject<JContainer>(strJson);
                        var retVal = new cF4SDHealthCardRawData.cHealthCardTable()
                        {
                            Name = constTableNameTicketHistory,
                            InformationClass = enumFasdInformationClass.Ticket,
                            Origin = enumDataHistoryOrigin.M42Wpm,
                            IsStatic = false,
                            TableType = eDataHistoryTableType.HistoryEvents
                        };

                        var hLength = ticketInfos.Count;
                        retVal.Columns = new Dictionary<string, cF4SDHealthCardRawData.cHealthCardTableColumn>(M42TableDetails.Columns.Count);

                        foreach (var colInfo in M42TableDetails.Columns.Values)
                        {
                            retVal.Columns[colInfo.Name] = new cF4SDHealthCardRawData.cHealthCardTableColumn(retVal)
                            {
                                ColumnName = colInfo.Name,
                                Values = new List<object>(hLength)
                            };
                        }

                        foreach (var Entry in ticketInfos)
                        {
                            var dicVals = new Dictionary<string, object>();
                            var vals = Entry.Children();
                            foreach (var token in vals)
                            {
                                try
                                {

                                    if (token is JProperty prop)
                                        if (prop.Value is JValue val)
                                            dicVals[prop.Name] = val.Value;
                                }
                                catch (Exception E)
                                {
                                    LogEntry($"Error getting M42 ticket detail property '{token.Path}'", LogLevels.Error);
                                    LogException(E);
                                }

                            }


                            foreach (var col in M42TableDetails.Columns.Values)
                            {
                                if (!retVal.Columns.TryGetValue(col.Name, out var dataCol))
                                    continue;
                                if (dicVals.TryGetValue(col.SourceName, out var _val))
                                {
                                    _val = ConvertToF4sdType(_val, col.ValueType);
                                    dataCol.Values.Add(_val);
                                }
                                else
                                    dataCol.Values.Add(null);
                            }
                        }

                        return new List<cF4SDHealthCardRawData.cHealthCardTable>(1) { retVal };
                    }
                }

                StartOnlineValidation();
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }

            return null;
        }

        private async Task<List<cF4SDHealthCardRawData.cHealthCardTable>> GetUserTicketAsync(Dictionary<enumFasdInformationClass, cF4sdConnectorIds> Identities, CancellationToken Token, cF4sdWebRequestInfo requestInfo, int LogDeep)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); }
            var _startTime = DateTime.UtcNow;

            try
            {
                // get table definition
                if (!Collector.ClusterConfig.Tables.TryGetValue(constTableNameUserTicket, out var tableConfig))
                    return null;

                // get the user sid
                if (!Identities.TryGetValue(enumFasdInformationClass.User, out var idUser))
                    return null;
                var strSid = new SecurityIdentifier(idUser.sid, 0).ToString();

                // get the asset name, if computer is in Identities
                if (!Identities.TryGetValue(enumFasdInformationClass.Computer, out var idComuter))
                    idComuter = null;

                // create the api url
                var strOpt = "";
                if (idComuter != null && !string.IsNullOrEmpty(idComuter.name))
                    strOpt = string.Format(constUrlGetCreateTicketAssetParam, HttpUtility.UrlEncode(idComuter.name));
                var strUrl = string.Format(constUrlGetCreateTicket, HttpUtility.UrlEncode(strSid), strOpt);

                // call the api
                var _wc = await GetWebClient(requestInfo, Token);
                var res = await _wc.HttpEnh.GetAsync(strUrl);

                if (res?.IsSuccessStatusCode == true)
                {
                    var strJson = await res.Content.ReadAsStringAsync();

                    if (!string.IsNullOrEmpty(strJson))
                    {
                        // construct the result
                        JObject ticketInfos = JsonConvert.DeserializeObject<JObject>(strJson);
                        var retVal = new cF4SDHealthCardRawData.cHealthCardTable()
                        {
                            Name = constTableNameUserTicket,
                            InformationClass = enumFasdInformationClass.User,
                            Origin = enumDataHistoryOrigin.M42Wpm,
                            IsStatic = true,
                            TableType = eDataHistoryTableType.Static
                        };

                        var dicVals = new Dictionary<string, object>();
                        var vals = ticketInfos.Children();
                        foreach (var token in vals)
                        {
                            try
                            {
                                if (token is JProperty prop)
                                    if (prop.Value is JValue val)
                                        dicVals[prop.Name] = val.Value;
                            }
                            catch (Exception E)
                            {
                                LogEntry($"Error getting M42 ticket detail property '{token.Path}'", LogLevels.Error);
                                LogException(E);
                            }

                        }

                        foreach (var colInfo in tableConfig.Columns.Values)
                        {
                            try
                            {
                                object objVal = null;
                                if (colInfo.Name == "id")
                                    objVal = idUser.sid;
                                else if (!dicVals.TryGetValue(colInfo.SourceName, out objVal))
                                    objVal = null;

                                objVal = ConvertToF4sdType(objVal, colInfo.ValueType);

                                retVal.Columns[colInfo.Name] = new cF4SDHealthCardRawData.cHealthCardTableColumn(retVal) { ColumnName = colInfo.Name, Values = new List<object>(1) { objVal } };
                            }
                            catch { }
                        }

                        return new List<cF4SDHealthCardRawData.cHealthCardTable>(1) { retVal };
                    }
                }

                StartOnlineValidation();
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }

            return null;
        }

        private async Task<List<cF4SDHealthCardRawData.cHealthCardTable>> GetUserPickupValuesAsync(cDataHistoryConfigTable TableConfig, cF4sdWebRequestInfo requestInfo, int LogDeep, CancellationToken Token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); }
            var _startTime = DateTime.UtcNow;

            try
            {
                if (!(TableConfig?.SubTables.Count > 0))
                    return null;
                if (!(TableConfig.SubTables.Values.First().Template is cDataHistoryConfigQueryTemplateM42Pickup _template))
                    return null;

                // create the api url
                var colInfo = new List<string>();
                foreach (var _colInfo in TableConfig.Columns.Values)
                {
                    colInfo.Add(_colInfo.SourceName);
                }
                var columns = string.Join(",", colInfo);
                var where = _template.FilteredGroup == -1 ? "" : string.Format("StateGroup = '{0}'", _template.FilteredGroup);
                var _strUrl = string.Format(constUrlFragmentsGetList, _template.PickupName, where, columns, string.Empty, string.Empty, string.Empty);
                _strUrl = RemoveEmptyQueryParameters(_strUrl);


                // call the api
                var _wc = await GetWebClient(requestInfo, Token);
                var res = await _wc.HttpEnh.GetAsync(_strUrl);

                if (res?.IsSuccessStatusCode == true)
                {
                    var strJson = await res.Content.ReadAsStringAsync();

                    if (!string.IsNullOrEmpty(strJson))
                    {
                        // construct the result
                        JArray PickupResult = JsonConvert.DeserializeObject<JArray>(strJson);
                        var retVal = new cF4SDHealthCardRawData.cHealthCardTable()
                        {
                            Name = TableConfig.Name,
                            InformationClass = enumFasdInformationClass.User,
                            Origin = enumDataHistoryOrigin.M42Wpm,
                            IsStatic = true,
                            TableType = eDataHistoryTableType.Static
                        };

                        var _dicColInfo = new Dictionary<string, cDataHistoryConfigColumnBase>(TableConfig.Columns.Count);
                        var _dicIndex = new Dictionary<string, int>(TableConfig.Columns.Count);
                        var _lstResultCols = new List<cHealthCardTableColumn>(TableConfig.Columns.Count);
                        var _idxPosition = -1;
                        var _idxName = -1;
                        var _i = 0;
                        foreach (var _colInfo in TableConfig.Columns.Values)
                        {
                            var _col = new cF4SDHealthCardRawData.cHealthCardTableColumn(retVal) { ColumnName = _colInfo.Name };
                            retVal.Columns[_colInfo.Name] = _col;
                            _dicColInfo[_colInfo.SourceName] = _colInfo;
                            _dicIndex[_colInfo.SourceName] = _i;
                            _lstResultCols.Add(_col);
                            if (_colInfo.SourceName == "Position")
                                _idxPosition = _i;
                            if (_colInfo.SourceName == "DisplayString")
                                _idxName = _i;
                            _i++;
                        }

                        var _result = new List<object[]>();
                        foreach (var _row in PickupResult)
                        {
                            var _s = _row.ToString();
                            try
                            {
                                // get the row values from JSON result
                                var IdsValid = true;
                                var _resultRow = new object[TableConfig.Columns.Count];
                                _resultRow.Initialize();
                                foreach (var _col in _row)
                                {
                                    if (_col is JProperty prop)
                                        if (prop.Value is JValue val)
                                        {
                                            if (_dicColInfo.TryGetValue(prop.Name, out var _colInfo))
                                                if (_dicIndex.TryGetValue(prop.Name, out var _idx))
                                                {
                                                    var objVal = ConvertToF4sdType(val.Value, _colInfo.ValueType);
                                                    if (TableConfig.KeyColumns.Contains(_colInfo))
                                                        if (objVal == null || objVal is string _str && _str == string.Empty)
                                                            IdsValid = false;

                                                    _resultRow[_idx] = objVal;
                                                }
                                        }
                                }

                                if (!IdsValid)
                                    continue;

                                _result.Add(_resultRow);

                            }
                            catch (Exception E)
                            {
                                LogEntry($"Error getting M42 pickup values for tale {TableConfig.Name} at '{_row}'", LogLevels.Error);
                                LogException(E);
                            }

                        }

                        // sort the result
                        switch (_template.Sorting)
                        {
                            case cDataHistoryConfigQueryTemplateM42Pickup.enumSorting.byPosition:
                                if (_idxPosition >= 0)
                                    try
                                    {
                                        _result.Sort((x, y) =>
                                        {
                                            var xVal = x[_idxPosition];
                                            var yVal = y[_idxPosition];

                                            // Wenn x null ist und y nicht, soll y zuerst kommen (x ist also größer)
                                            if (xVal == null && yVal != null) return 1;

                                            // Wenn y null ist und x nicht, soll x zuerst kommen (x ist also kleiner)
                                            if (xVal != null && yVal == null) return -1;

                                            // Wenn beide null sind, sind sie gleich
                                            if (xVal == null && yVal == null) return 0;

                                            // Wenn keines der beiden null ist, normal vergleichen
                                            return ((Int32)xVal).CompareTo((Int32)yVal);
                                        });
                                    }
                                    catch { }
                                break;
                            case cDataHistoryConfigQueryTemplateM42Pickup.enumSorting.byName:
                                if (_idxName >= 0)
                                    try
                                    {
                                        _result.Sort((x, y) => ((string)(x[_idxName])).CompareTo((string)(y[_idxName])));
                                    }
                                    catch { }
                                break;
                        }

                        // put the result into the result colums
                        foreach (var _entry in _result)
                        {
                            for (int _idx = 0; _idx < TableConfig.Columns.Count; _idx++)
                            {
                                var _col = _lstResultCols[_idx];
                                _col.Values.Add(_entry[_idx]);
                            }
                        }

                        return new List<cF4SDHealthCardRawData.cHealthCardTable>(1) { retVal };
                    }
                }

                StartOnlineValidation();
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }

            return null;
        }

        public override bool CheckIfLateDelivery(cDataHistoryConfigTable Table)
        {
            if (Table.Name == constTableNameTicketHistory)
                return true;
            if (Table.SubTables?.First().Value?.Template is cDataHistoryConfigQueryTemplateM42Pickup)
                return true;

            return false;
        }

        public override async Task<bool> WritePropertyAsync(cF4SDWriteParameters PropertiesToWrite, cF4sdWebRequestInfo requestInfo, CancellationToken Token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            try
            {
                if (PropertiesToWrite.TableName != constTableNameTicketDetails)
                    return false;

                if (PropertiesToWrite.Values.Count != 1)
                    return false;

                if (!Collector.ClusterConfig.Tables.TryGetValue(constTableNameTicketDetails, out var tableConfig))
                    return false;

                if (!tableConfig.Columns.TryGetValue(PropertiesToWrite.Values.First().Key, out var colInfo))
                    return false;

                if (!colInfo.IsWritable)
                    return false;

                var strSolution = JsonConvert.SerializeObject(PropertiesToWrite.Values.First().Value.ToString());
                var _content = new StringContent(strSolution);
                var _url = string.Format(constUrlUpdateSolution, HttpUtility.UrlEncode(PropertiesToWrite.id.ToString()));

                var _wc = await GetWebClient(requestInfo, Token);
                var res = await _wc.HttpEnh.PostAsync(_url, _content, Token);

                if (res?.IsSuccessStatusCode == true)
                    return true;

                StartOnlineValidation();

                return false;
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (CM != null) LogMethodEnd(CM);
            }

            return false;
        }

        private async void onlineTimerCallback(object state)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            try
            {
                CleanupUserWebCache();
                await ValidateConnectionAsync(false, CancellationToken.None);
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (CM != null) LogMethodEnd(CM);
            }

        }

        private void StartOnlineValidation()
        {
            if (Collector?.autoConnectionCheck == true)
                _ = Task.Run(() => ValidateConnectionAsync(false, CancellationToken.None));
        }

        public async Task<bool> CheckOnline()
        {
            await _OnlineCheckCriticalSection.Semaphore.WaitAsync();
            try
            {
                return IsOnline;
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                _OnlineCheckCriticalSection.Semaphore.Release();
            }

            return false;
        }

        public async Task<M42UserUserInfo> GetM42UserInfoAsync(string userSid, CancellationToken Token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            try
            {
                var strUrl = string.Format(constUrlGetRoleMeberships, userSid);
                var res = await WebClient.HttpEnh.GetAsync(strUrl, Token);
                if (Token.IsCancellationRequested)
                    return null;

                if (res?.IsSuccessStatusCode == true)
                {
                    var strJson = await res.Content.ReadAsStringAsync();

                    if (!string.IsNullOrEmpty(strJson))
                    {
                        var _userInfo = JsonConvert.DeserializeObject<M42UserUserInfo>(strJson);
                        if (_userInfo?.User != null && _userInfo.User.Id != Guid.Empty)
                            return _userInfo;
                    }
                }


            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (CM != null) LogMethodEnd(CM);
            }

            return null;
        }

        private async Task<List<string>> GetUserRoles(Guid UserId, CancellationToken Token, string ReportTag = null, bool logDetailed = false)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            try
            {
                var _QueryRelationInfo = new cM42DataQueryRelationInfo2()
                {
                    EntityClassName = "SPSUserClassBase",
                    EntityTypeName = "SPSSecurityTypeRole",
                    FragmentId = UserId,
                    columns = "Name",
                    pageSize = 0,
                    RelationName = "MemberOf"
                };

                var _json = JsonConvert.SerializeObject(_QueryRelationInfo);
                var _content = new StringContent(_json, Encoding.UTF8, "application/json");
                var _res = await WebClient.HttpEnh.PostAsync(constUrlGetDataQueryRelationItems, _content, Token);
                if (_res.IsSuccessStatusCode)
                {
                    var _strResult = await _res.Content.ReadAsStringAsync();

                    if (string.IsNullOrEmpty(_strResult))
                        return null;

                    List<JObject> _roles = JsonConvert.DeserializeObject<List<JObject>>(_strResult);

                    var _retVal = new List<string>();

                    foreach (var _roleEntry in _roles)
                    {
                        var _vals = _roleEntry.Children();
                        foreach (var _val in _vals)
                        {
                            try
                            {
                                if (_val is JProperty prop)
                                {
                                    if (prop.Name == "Name")
                                    {
                                        var _propGroupName = prop.Value;
                                        if (prop.Value is JValue _jVal && _jVal.Value != null)
                                        {
                                            var _groupName = _jVal.Value.ToString();
                                            _retVal.Add(_groupName);
                                        }
                                    }
                                }
                            }
                            catch (Exception E)
                            {
                                LogException(E);
                            }
                        }
                    }

                    if (_retVal.Count > 0)
                        return _retVal;
                }

            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (CM != null) LogMethodEnd(CM);
            }

            return null;
        }

        private async Task<List<cF4SDMembershipGroup>> GetF4SDRoles(Guid UserId, CancellationToken Token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            try
            {
                var _M42UserInfoRoles = await GetUserRoles(UserId, Token);

                if (_M42UserInfoRoles != null && _M42UserInfoRoles.Count > 0)
                {
                    var _M42Memberships = Collector?.InfrastructureConfig?.Authorisation?.MembershipGroups?.GroupsM42;

                    var _resMembershipGroups = new List<cF4SDMembershipGroup>();

                    if (_M42Memberships != null)
                    {
                        foreach (var _membership in Collector.InfrastructureConfig.Authorisation.MembershipGroups.GroupsM42.Values)
                        {
                            foreach (var _M42Role in _M42UserInfoRoles)
                            {

                                if (_M42Role.ToLowerInvariant() == _membership.RoleName.ToLowerInvariant())
                                    if (!_resMembershipGroups.Contains(_membership))
                                        _resMembershipGroups.Add(_membership);
                            }
                        }

                    }

                    return _resMembershipGroups;
                }
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (CM != null) LogMethodEnd(CM);
            }

            return null;
        }

        public async Task AddM42UserInfosAsync(cF4sdUserInfo userInfo, CancellationToken Token, cF4sdWebRequestInfo requestInfo, int LogDeep)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); }
            var _startTime = DateTime.UtcNow;

            try
            {
                // set M42 Logon to possible
                if (userInfo.possibleLogons == null)
                    userInfo.possibleLogons = new List<enumAdditionalAuthentication>();
                if (!userInfo.possibleLogons.Contains(enumAdditionalAuthentication.M42WinLogon))
                    userInfo.possibleLogons.Add(enumAdditionalAuthentication.M42WinLogon);

                // check, if the ad sid can be found in M42
                var _M42UserInfo = await GetM42UserInfoAsync(userInfo.AdSid, Token);

                if (Token.IsCancellationRequested)
                    return;

                // if yes, set M42 for an addidional automatic logon possibility
                if (_M42UserInfo?.User != null)
                {
                    if (userInfo.additionalLogons == null)
                        userInfo.additionalLogons = new List<enumAdditionalAuthentication>();
                    if (!userInfo.additionalLogons.Contains(enumAdditionalAuthentication.M42WinLogon))
                        userInfo.additionalLogons.Add(enumAdditionalAuthentication.M42WinLogon);

                }

                // check, if we have already a valid token for the user
                if (Collector.TokenCache != null)
                {
                    // get the api token from token cache
                    cM42UserInfoResult m42UserInfo = null;
                    var _res = await Collector.TokenCache.GetAsync(userInfo.Id, cF4SDTokenRegistration.enumTokenType.M42Api, requestInfo, LogDeep + 1);
                    if (_res != null && _res.IsValid())
                    {
                        // check the api token, if it's still valid
                        var _m42 = new cM42WebClient(Collector.InfrastructureConfig.M42Wpm.Server, UseSSL: true);
                        var _m42Reason = await _m42.LogonWithApiKeyAsync(_res.secret, Token);
                        if (_m42Reason == eReason.ok && _m42.BearerToken != null)
                        {
                            // get the user info
                            m42UserInfo = await _m42.GetUserInfoAsync();

                            // save the new bearer token to token cache
                            var TokenRegistration = new cF4SDTokenRegistration()
                            {
                                TokenType = cF4SDTokenRegistration.enumTokenType.M42Bearer,
                                UserId = userInfo.Id,
                                Secret = _m42.BearerToken,
                                IdRemote = m42UserInfo.User.ObjectID?.ToString("D")
                            };

                            // set the new bearer token into the token cache
                            await Collector.TokenCache.SetAsync(TokenRegistration, GetTokenValidityPeriod(_m42.BearerLifeTime), Token, requestInfo, LogDeep + 1);
                            if (Token.IsCancellationRequested)
                                return;

                            // set the valid until info
                            if (userInfo.ValidLogonsUntil == null)
                                userInfo.ValidLogonsUntil = new Dictionary<enumAdditionalAuthentication, cF4SDTokenValidityPeriod>();
                            userInfo.ValidLogonsUntil[enumAdditionalAuthentication.M42WinLogon] = cF4SDTokenValidityPeriod.Create(_res.validUntil, _res.renewUntil);
                        }

                    }
                    else
                    {
                        // check, if there is a valid bearer token for M42
                        _res = await Collector.TokenCache.GetAsync(userInfo.Id, cF4SDTokenRegistration.enumTokenType.M42Bearer, requestInfo, LogDeep + 1);
                        if (_res != null && _res.IsValid())
                        {
                            // if yes, get the user info
                            var _m42 = new cM42WebClient(Collector.InfrastructureConfig.M42Wpm.Server, UseSSL: true);
                            _m42.SetBearerToken(_res.secret);

                            var _res2 = await _m42.Open();
                            if (_res2 == eReason.ok)
                            {
                                m42UserInfo = await _m42.GetUserInfoAsync();

                                // set the valid until info
                                if (userInfo.ValidLogonsUntil == null)
                                    userInfo.ValidLogonsUntil = new Dictionary<enumAdditionalAuthentication, cF4SDTokenValidityPeriod>();
                                userInfo.ValidLogonsUntil[enumAdditionalAuthentication.M42WinLogon] = cF4SDTokenValidityPeriod.Create(_res.validUntil, _res.renewUntil);
                            }

                        }
                    }

                    // get the role memberships relatet to the M42 roles
                    if (m42UserInfo?.User?.Id != null)
                    {
                        var _F4SDRoles = await GetF4SDRoles((Guid)m42UserInfo.User.Id, Token);

                        if (_F4SDRoles?.Count > 0)
                            Collector.AddRolesByMembership(userInfo, _F4SDRoles);

                        return;
                    }
                }


            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }
        }

        public override async Task<Int64> GetSelectionTableResultCountAsync(cDataHistoryConfigTable Table, List<cF4sdIdentityEntry> Identities, string search, cF4sdWebRequestInfo requestInfo, CancellationToken Token, bool resetFilter = false, List<string> filterParams = null)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            try
            {
                if (!(Table.SubTables.Values.First().Template is cDataHistoryConfigQueryTemplateM42DataQueryItems _template))
                    return -1;

                if (string.IsNullOrEmpty(search))
                    search = "";
                string _whereExpr = null;
                if (!resetFilter && !string.IsNullOrEmpty(_template.WhereExpression))
                {
                    if (filterParams != null && filterParams.Count > 0)
                        _whereExpr = string.Format(_template.WhereExpression, filterParams.ToArray());
                    else
                        _whereExpr = _template.WhereExpression;
                }
                var _cols = Table.KeyColumns.First().SourceName;

                var _QueryRelationInfo = new cM42DataQueryRelationInfo()
                {
                    EntityClassName = _template.EntityClassName,
                    EntityTypeName = string.Join(",", _template.EntityTypeNames),
                    DataQueryName = string.IsNullOrWhiteSpace(_template.DataQueryName) ? null : _template.DataQueryName,
                    search = search,
                    WhereExpression = _whereExpr,
                    pageSize = 0,
                    page = 0,
                    totalCounted = true,
                    columns = _cols
                };

                var _json = JsonConvert.SerializeObject(_QueryRelationInfo);
                var _content = new StringContent(_json, Encoding.UTF8, "application/json");
                var _wc = await GetWebClient(requestInfo, Token);
                var res = await _wc.HttpEnh.PostAsync(constUrlGetDataQueryRelationItemsCount, _content);
                if (res.IsSuccessStatusCode)
                {
                    var _strResult = await res.Content.ReadAsStringAsync();
                    if (Int64.TryParse(_strResult, out var count))
                    {
                        return count;
                    }
                }
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (CM != null) LogMethodEnd(CM);
            }

            return -1;
        }

        public override async Task<cF4SDHealthCardRawData.cHealthCardTable> GetSelectionTableResultAsync(cDataHistoryConfigTable Table, List<cF4sdIdentityEntry> Identities, string search, int PageSize, int Page, cF4sdWebRequestInfo requestInfo, CancellationToken Token, bool resetFilter = false, List<string> filterParams = null)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            try
            {
                if (!(Table.SubTables.Values.First().Template is cDataHistoryConfigQueryTemplateM42DataQueryItems _template))
                    return null;

                if (string.IsNullOrEmpty(search))
                    search = "";
                string _whereExpr = null;
                if (!resetFilter && !string.IsNullOrEmpty(_template.WhereExpression))
                {
                    if (filterParams != null && filterParams.Count > 0)
                        _whereExpr = string.Format(_template.WhereExpression, filterParams.ToArray());
                    else
                        _whereExpr = _template.WhereExpression;
                }

                var _cols = Table.KeyColumns.First().SourceName;

                var _QueryRelationInfo = new cM42DataQueryRelationInfo()
                {
                    EntityClassName = _template.EntityClassName,
                    EntityTypeName = string.Join(",", _template.EntityTypeNames),
                    DataQueryName = string.IsNullOrWhiteSpace(_template.DataQueryName) ? null : _template.DataQueryName,
                    WhereExpression = _whereExpr,
                    search = search,
                    pageSize = PageSize,
                    page = Page,
                    columns = _cols
                };

                var _json = JsonConvert.SerializeObject(_QueryRelationInfo);
                var _content = new StringContent(_json, Encoding.UTF8, "application/json");
                var _wc = await GetWebClient(requestInfo, Token);
                var res = await _wc.HttpEnh.PostAsync(constUrlGetDataQueryRelationItems, _content, Token);
                if (res.IsSuccessStatusCode)
                {
                    var _strResult = await res.Content.ReadAsStringAsync();

                    if (string.IsNullOrEmpty(_strResult))
                        return null;

                    List<JObject> ticketInfos = JsonConvert.DeserializeObject<List<JObject>>(_strResult);
                    var _valueCount = ticketInfos.Count;
                    var retVal = new cF4SDHealthCardRawData.cHealthCardTable()
                    {
                        Name = Table.Name,
                        InformationClass = Table.ParentCluster.InformationClass,
                        Origin = Table.ParentCluster.Origin,
                        IsStatic = false,
                        TableType = Table.Type
                    };

                    var _dicColInfo = Table.Columns.ToDictionary(v => v.Value.SourceName, v => v.Value);
                    foreach (var colInfo in Table.Columns.Values)
                    {
                        retVal.Columns[colInfo.Name] = new cF4SDHealthCardRawData.cHealthCardTableColumn(retVal) { ColumnName = colInfo.Name, Values = new List<object>(_valueCount) };
                    }

                    foreach (var _entry in ticketInfos)
                    {
                        var _vals = _entry.Children();
                        var _maxCount = 0;
                        foreach (var _token in _vals)
                        {
                            try
                            {
                                if (_token is JProperty prop)
                                    if (!string.IsNullOrEmpty(prop.Name) && prop.Value is JValue _val)
                                    {
                                        var _value = _val.Value;

                                        if (!_dicColInfo.TryGetValue(prop.Name, out var _colInfo))
                                            continue;
                                        if (!retVal.Columns.TryGetValue(_colInfo.Name, out var _colData))
                                            continue;

                                        _value = ConvertToF4sdType(_value, _colInfo.ValueType);

                                        _colData.Values.Add(_value);
                                        _maxCount = Math.Max(_maxCount, _colData.Values.Count);
                                    }
                            }
                            catch (Exception E)
                            {
                                LogEntry($"Error getting M42 ticket detail property '{_token?.Path}'", LogLevels.Error);
                                LogException(E);
                            }
                        }

                        foreach (var _colData in retVal.Columns.Values)
                        {
                            while (_colData.Values.Count < _maxCount)
                                _colData.Values.Add(null);
                        }
                    }

                    return retVal;

                }
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (CM != null) LogMethodEnd(CM);
            }

            return null;
        }

        private cF4SDTokenValidityPeriod GetTokenValidityPeriod(DateTime validUntil)
        {
            var _renew = validUntil - TimeSpan.FromSeconds(Math.Max(0, (validUntil - DateTime.UtcNow).TotalSeconds * 2 / 10));
            return cF4SDTokenValidityPeriod.Create(validUntil, _renew);
        }

        public override async Task<cF4SdUserInfoChange> ValidateTokenAsync(cF4SDTokenRegistration TokenRegistration, cF4sdWebRequestInfo requestInfo, int LogDeep, CancellationToken Token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); }
            var _startTime = DateTime.UtcNow;

            try
            {
                if (TokenRegistration?.TokenType is cF4SDTokenRegistration.enumTokenType.M42Bearer &&
                    !string.IsNullOrEmpty(Collector?.InfrastructureConfig?.M42Wpm.Server) &&
                    !string.IsNullOrEmpty(TokenRegistration?.Secret)
                   )
                {
                    var _M42UserInfo = await cM42WebClient.LogonBearer(Collector.InfrastructureConfig.M42Wpm.Server, TokenRegistration.Secret, Token, UseSSL: true, LogDetailed: true);
                    if (Token.IsCancellationRequested)
                        return null;

                    if (_M42UserInfo != null)
                    {
                        if (_M42UserInfo.BearerValidUntil > DateTime.UtcNow && TokenRegistration.AutoCreatePermanent && _M42UserInfo.User?.Id != null)
                        {
                            TokenRegistration.IdRemote = _M42UserInfo.User.ObjectID?.ToString("D");
                            var _res2 = await CreateApiToken(TokenRegistration, _M42UserInfo, Token, requestInfo, LogDeep + 1);
                            if (Token.IsCancellationRequested)
                                return null;

                            if (_res2 < DateTime.UtcNow)
                                return null;
                        }

                        // user info & f4sd token aktualisieren
                        var _validityPeriod = GetTokenValidityPeriod(_M42UserInfo.BearerValidUntil);


                        var Roles = new List<string>();
                        if (_M42UserInfo?.User?.Id != null)
                        {
                            var _F4SDRoles = await GetF4SDRoles((Guid)_M42UserInfo.User.Id, Token);
                            if (_F4SDRoles?.Count > 0)
                                Collector.AddRolesByMembership(Roles, _F4SDRoles, requestInfo?.userInfo?.Name);

                        }
                        var _retVal = new cF4SdUserInfoChange()
                        {
                            ValidLogonsUntil = new Dictionary<enumAdditionalAuthentication, cF4SDTokenValidityPeriod> { { enumAdditionalAuthentication.M42WinLogon, _validityPeriod } },
                            Roles = Roles
                        };


                        return _retVal;
                    }
                }
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }

            return null;
        }

        private class cM42IdResult
        {
            public Guid Id { get; set; }
            public string Name { get; set; }
        }

        private async Task<DateTime> CreateApiToken(cF4SDTokenRegistration TokenRegistration, cM42UserInfoResult UserInfo, CancellationToken Token, cF4sdWebRequestInfo requestInfo, int LogDeep)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); }
            var _startTime = DateTime.UtcNow;

            try
            {
                // obtain token name
                var tokenName = $"F4SD_{UserInfo?.User?.LastName ?? ""}_{UserInfo?.User?.FirstName ?? ""}_{UserInfo?.User?.Id}";

                // create M42 web client
                var _m42 = new cM42WebClient(Collector.InfrastructureConfig.M42Wpm.Server, UseSSL: true);
                _m42.SetBearerToken(UserInfo.BearerToken);
                var _resOpen = await _m42.Open();
                if (_resOpen != eReason.ok)
                    return DateTime.MinValue;

                // check if token alsready exist
                var _strListToken = await _m42.GetFragmentListAsync("PLSTSApiTokenBase", Token, $"Name='{tokenName}'");
                if (!string.IsNullOrEmpty(_strListToken))
                {
                    // remove all existing tokens with corresponding name
                    var lstIds = JsonConvert.DeserializeObject<List<cM42IdResult>>(_strListToken);
                    foreach (var objId in lstIds)
                    {
                        var _delRequest = new Dictionary<Guid, string>()
                        {
                            { objId.Id, "PLSTSApiTokenType" }
                        };
                        await _m42.DeleteObjectsAsync("F4SD Cockpit", _delRequest, Token);
                        if (Token.IsCancellationRequested)
                            return DateTime.MinValue;
                    }
                }

                // create token generation request
                var tokenRequest = new cApiTokenRequestModelBase()
                {
                    Name = tokenName,
                    ExpirationDays = (int)(Collector?.InfrastructureConfig?.M42Wpm?.ApiLifetime ?? cDataHistoryConfigM42Wpm.enumApiTokenLifetime.days30)
                };

                // generate token
                var strJson = JsonConvert.SerializeObject(tokenRequest);
                var CO = new StringContent(strJson, Encoding.UTF8, "application/json");
                var _resPost = await _m42.Http.PostAsync(constUrlGenerateApiTokenForMe, CO);
                if (Token.IsCancellationRequested)
                    return DateTime.MinValue;

                var _content = await _resPost.Content.ReadAsStringAsync();
                if (Token.IsCancellationRequested)
                    return DateTime.MinValue;

                if (_resPost.IsSuccessStatusCode)
                {
                    var _tokenModel = JsonConvert.DeserializeObject<cM42ApiTokenModel>(_content);
                    if (!string.IsNullOrEmpty(_tokenModel?.ApiToken) && _tokenModel?.ValidUntil >= DateTime.UtcNow)
                    {
                        var _validUntil = (DateTime)_tokenModel.ValidUntil;
                        var _renewUntil = _validUntil - TimeSpan.FromDays(1);
                        var _tr = new cF4SDTokenRegistration()
                        {
                            TokenType = cF4SDTokenRegistration.enumTokenType.M42Api,
                            UserId = TokenRegistration.UserId,
                            Name = _tokenModel?.Name,
                            Secret = _tokenModel.ApiToken,
                            IdRemote = UserInfo?.User?.Id?.ToString("D"),
                            AutoCreatePermanent = false
                        };

                        await Collector.TokenCache.SetAsync(_tr, cF4SDTokenValidityPeriod.Create(_validUntil, _renewUntil), Token, requestInfo, LogDeep + 1);
                        if (Token.IsCancellationRequested)
                            return DateTime.MinValue;

                        return _validUntil;
                    }
                }

            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }

            return DateTime.MinValue;
        }

        public async Task<string> CreateM42TicketAsync(cApiM42Ticket ticketInfo, cF4sdWebRequestInfo requestInfo, int LogDeep, CancellationToken token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); }
            var _startTime = DateTime.UtcNow;

            try
            {
                var _webClient = await GetUserWebClient(requestInfo, LogDeep + 1, token);
                if (token.IsCancellationRequested || _webClient == null) return null;


                var userFragmentListTask = GetFragmentListAsync(new cDataFragmentsParams
                {
                    Columns = "ID",
                    Ddname = "SPSUserClassBase",
                    Pagenumber = 0,
                    Pagesize = 10,
                    Where = string.Format("T(SPSCommonClassbase).State = 2023 AND Accounts.T(SPSAccountClassAD).ADCn = '{0}'", ticketInfo.User)
                }, requestInfo, LogDeep + 1, token);

                List<Task> tasksToAwait = new List<Task> { userFragmentListTask };

                Task<dynamic> assetFragmentListTask = null;
                if (!ticketInfo.AffectedAsset.Equals("RemoveAffectedAssetEntry", StringComparison.InvariantCultureIgnoreCase) && !string.IsNullOrEmpty(ticketInfo.AffectedAsset))
                {
                    assetFragmentListTask = GetFragmentListAsync(new cDataFragmentsParams
                    {
                        Columns = "ID",
                        Ddname = "SPSAssetClassBase",
                        Pagenumber = 0,
                        Pagesize = 10,
                        Where = string.Format("T(SPSCommonClassbase).State = 710 AND T(SPSComputerClassBase).Name = '{0}'", ticketInfo.AffectedAsset)
                    }, requestInfo, LogDeep + 1, token);
                    tasksToAwait.Add(assetFragmentListTask);
                }

                Task<dynamic> quickCallFragmentListTask = null;
                if (ticketInfo.Quickcall.HasValue && ticketInfo.Quickcall.Value != Guid.Empty)
                {
                    quickCallFragmentListTask = GetQuickCallTemplateInfoAsync(ticketInfo.Quickcall.Value, requestInfo, LogDeep + 1, token);
                    tasksToAwait.Add(quickCallFragmentListTask);
                }
                else
                {
                    LogEntry($"No quick call set to create new ticket.", LogLevels.Error);
                    return null;
                }

                // Warte auf alle gestarteten Tasks
                await Task.WhenAll(tasksToAwait);

                var fragments = userFragmentListTask.Result;
                var idAsString = "";
                Guid? assetId = null;
                var assetNotFound = false;
                if (assetFragmentListTask != null)
                {
                    var assetFragments = assetFragmentListTask.Result;
                    if (token.IsCancellationRequested) return null;
                    if (assetFragments == null || assetFragments.Count == 0)
                    {
                        assetNotFound = true;
                    }
                    else
                    {
                        idAsString = (string)assetFragments?[0]?.ID;
                        if (Guid.TryParse(idAsString, out Guid assetIdTemp))
                        {
                            assetId = assetIdTemp;
                        }
                        else
                            assetNotFound = true;
                    }
                }

                idAsString = (string)fragments?[0]?.ID;
                if (!Guid.TryParse(idAsString, out Guid userID))
                {
                    LogEntry($"No initiating user found with state active (710) and AD Common Name: {ticketInfo.User}", LogLevels.Error);
                    return null;
                }
                Guid? requestCategoryId = ticketInfo.Category;
                if (requestCategoryId.HasValue && requestCategoryId.Value == Guid.Empty)
                {
                    requestCategoryId = null;
                }

                var ticketInfo2 = new cM42TicketInfo()
                {
                    Subject = ticketInfo.Summary,
                    DescriptionHTML = ticketInfo.DescriptionHtml,
                    SolutionHTML = ticketInfo.SolutionHtml,
                    User = userID,
                    Asset = assetId,
                    Service = ticketInfo.AffectedService,
                    Template = ticketInfo.Quickcall,
                    Category = requestCategoryId,
                    EntryBy = 20500
                };

                var quickCallFragments = quickCallFragmentListTask?.Result;
                if (quickCallFragments != null)
                {
                    var quickCallData = quickCallFragments;

                    if (int.TryParse((string)quickCallData?.Impact, out int impact))
                        ticketInfo2.Impact = impact;

                    if (int.TryParse((string)quickCallData?.Urgency, out int urgency))
                        ticketInfo2.Urgency = urgency;

                    if (int.TryParse((string)quickCallData?.EntryBy, out int entryBy))
                        ticketInfo2.EntryBy = entryBy;

                    if (int.TryParse((string)quickCallData?.ErrorType, out int errorType))
                        ticketInfo2.ErrorType = errorType;

                    if (int.TryParse((string)quickCallData?.Priority, out int priority))
                        ticketInfo2.Priority = priority;

                    if (Guid.TryParse((string)quickCallData?.RecipientRole, out Guid recipientRole))
                        ticketInfo2.ResponsibleRole = recipientRole;

                    if (Guid.TryParse((string)quickCallData?.RecipientID, out Guid recipient))
                        ticketInfo2.ResponsibleUser = recipient;

                    if (Guid.TryParse((string)quickCallData?.Asset, out Guid asset))
                        ticketInfo2.Asset = ticketInfo2.Asset ?? asset;

                    if (Guid.TryParse((string)quickCallData?.Service, out Guid service))
                        ticketInfo2.Service = ticketInfo2.Service ?? service;

                    if (!ticketInfo2.Category.HasValue && Guid.TryParse((string)quickCallData?.Category, out Guid category))
                        ticketInfo2.Category = category;

                    if (bool.TryParse((string)quickCallData?.NotifyResponsible, out bool notifyResponsible))
                        ticketInfo2.NotifyResponsible = notifyResponsible;

                    if (int.TryParse((string)quickCallData?.Classification, out int Classification))
                        ticketInfo2.Classification = Classification;
                }

                var _jsonContent = JsonConvert.SerializeObject(ticketInfo2, Formatting.None,
                    new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
                var _content = new StringContent(_jsonContent, Encoding.UTF8, "application/json");

                var url = string.Format(constUrlCreateTicket, ticketInfo2.Classification);


                var response = await _webClient.HttpEnh.PostAsync(url, _content, token);
                if (token.IsCancellationRequested) return null;

                if (response.IsSuccessStatusCode)
                {
                    var json = await response.Content.ReadAsStringAsync();
                    Guid TicketId = Guid.TryParse(json?.Trim('"'), out TicketId) ? TicketId : Guid.Empty;

                    await AddWorkingTimesAsync(TicketId, ticketInfo.workTimes, requestInfo, LogDeep + 1, token);

                    cCreateJournalEntryInfo journalEntry = null;
                    if (assetNotFound)
                    {
                        string assetNotFoundText = "";
                        if (ticketInfo.AdditionalValues.TryGetValue("Dialog.CloseCase.AssetNotFoundInMatrix42", out object valueObj))
                        {
                            assetNotFoundText = valueObj as string;
                        }
                        if (!string.IsNullOrEmpty(assetNotFoundText))
                        {
                            journalEntry = new cCreateJournalEntryInfo()
                            {
                                Comments = string.Format(assetNotFoundText, ticketInfo.AffectedAsset),
                                ObjectId = TicketId,
                                EntryType = 0
                            };
                            await CreateM42JournalAsync(journalEntry, requestInfo, LogDeep + 1, token);
                        }
                    }
                    journalEntry = new cCreateJournalEntryInfo()
                    {
                        Comments = ticketInfo.CopyTemplates,
                        ObjectId = TicketId,
                        EntryType = 20600
                    };
                    await CreateM42JournalAsync(journalEntry, requestInfo, LogDeep + 1, token);
                    if (token.IsCancellationRequested) return null;

                    try
                    {
                        if (ticketInfo.SupportCaseProtocollEntries != null)
                        {
                            await ProtocollSupportCaseProtocollEntries(ticketInfo.SupportCaseProtocollEntries, TicketId, requestInfo, LogDeep, token);
                        }
                        else if (ticketInfo.QuickActionHistory != null)
                        {
                            await ProtocollSupportCaseQuickActionHistory(ticketInfo.QuickActionHistory, TicketId, requestInfo, LogDeep, token);
                        }

                        if (token.IsCancellationRequested)
                            return null;
                    }
                    catch (TaskCanceledException)
                    {
                        return null;
                    }

                    ticketInfo.Ticket = TicketId;
                    await UpdateM42TicketUnreadAsync(ticketInfo, requestInfo, LogDeep + 1, token);

                    return TicketId.ToString();

                }
            }
            catch (Exception e)
            {
                LogException(e);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }

            return null;
        }

        private async Task ProtocollSupportCaseProtocollEntries(List<ProtocollEntryBase> supportCaseProtocollEntries, Guid ticketId, cF4sdWebRequestInfo requestInfo, int logDeep, CancellationToken token)
        {
            foreach (var protocollEntry in supportCaseProtocollEntries.OrderBy(entry => entry.Time))
            {
                cCreateJournalEntryInfo _entry = null;
                if (protocollEntry is QuickActionProtocollEntry quickActionEntry)
                {
                    switch (quickActionEntry.ResultCode)
                    {
                        case -1:
                        case 1:
                        case 0:
                            var entryType = quickActionEntry.WasRunningOnAffectedDevice ? 20602 : 20601;
                            _entry = new cCreateJournalEntryInfo()
                            {
                                Comments = "",
                                ObjectId = ticketId,
                                EntryType = entryType,
                                Parameters = new List<cCreateJournalEntryInfo.Parameter>() { new cCreateJournalEntryInfo.Parameter()
                                        {
                                            Name = "result",
                                            Value = quickActionEntry.HtmlContent ?? quickActionEntry.MeasureValues ?? quickActionEntry.ErrorMessage
                                        },
                                        new cCreateJournalEntryInfo.Parameter()
                                        {
                                            Name = "quickActionName",
                                            Value = quickActionEntry.Name
                                        },
                                        new cCreateJournalEntryInfo.Parameter()
                                        {
                                            Name = "QuickActionId",
                                            Value = quickActionEntry.Id
                                        },
                                        new cCreateJournalEntryInfo.Parameter()
                                        {
                                            Name = "quickActionExecutionType",
                                            Value = quickActionEntry.ExecutionTypeId
                                        },
                                        new cCreateJournalEntryInfo.Parameter()
                                        {
                                            Name = "ExecutionTime",
                                            Value = quickActionEntry.Time
                                        },
                                        new cCreateJournalEntryInfo.Parameter()
                                        {
                                            Name = "AffectedDeviceName",
                                            Value = quickActionEntry.AffectedDeviceName
                                        },}
                            };
                            break;
                        default:
                            break;
                    }
                }
                else if (protocollEntry is QuickTipStepProtocollEntry quickTipEntry)
                {
                    _entry = new cCreateJournalEntryInfo()
                    {
                        Comments = "",
                        ObjectId = ticketId,
                        EntryType = 20605,
                        Parameters = new List<cCreateJournalEntryInfo.Parameter>() { new cCreateJournalEntryInfo.Parameter()
                                {
                                    Name = "result",
                                    Value = quickTipEntry.HtmlContent ?? quickTipEntry.AsciiContent
                                },
                                new cCreateJournalEntryInfo.Parameter()
                                {
                                    Name = "quickTipName",
                                    Value = quickTipEntry.Name
                                },
                                new cCreateJournalEntryInfo.Parameter()
                                {
                                    Name = "QuickTipId",
                                    Value = quickTipEntry.Id
                                },
                                new cCreateJournalEntryInfo.Parameter()
                                {
                                    Name = "ExecutionTime",
                                    Value = quickTipEntry.Time
                                },
                                new cCreateJournalEntryInfo.Parameter()
                                {
                                    Name = "WasSuccessfull",
                                    Value = quickTipEntry.WasSuccessfull
                                },}
                    };
                }

                if (_entry != null)
                {
                    await CreateM42JournalAsync(_entry, requestInfo, logDeep + 1, token);
                    if (token.IsCancellationRequested) return;
                }
            }
        }

        private async Task ProtocollSupportCaseQuickActionHistory(List<cQuickActionResult> quickActionHistory, Guid ticketId, cF4sdWebRequestInfo requestInfo, int logDeep, CancellationToken token)
        {
            foreach (var entry in quickActionHistory)
            {
                cCreateJournalEntryInfo _entry = null;
                switch (entry.ResultCode)
                {
                    case -1:
                    case 1:
                    case 0:
                        var entryType = entry.wasRunningOnAffectedDevice ? 20602 : 20601;
                        _entry = new cCreateJournalEntryInfo()
                        {
                            Comments = "",
                            ObjectId = ticketId,
                            EntryType = entryType,
                            Parameters = new List<cCreateJournalEntryInfo.Parameter>() { new cCreateJournalEntryInfo.Parameter()
                                    {
                                        Name = "result",
                                        Value = entry.Output ?? entry.MeasureValues ?? entry.ErrorMessage
                                    },
                                    new cCreateJournalEntryInfo.Parameter()
                                    {
                                        Name = "quickActionName",
                                        Value = entry.QuickActionName
                                    },
                                    new cCreateJournalEntryInfo.Parameter()
                                    {
                                        Name = "QuickActionId",
                                        Value = entry.QuickActionId
                                    },
                                    new cCreateJournalEntryInfo.Parameter()
                                    {
                                        Name = "quickActionExecutionType",
                                        Value = entry.QuickActionExecutionType
                                    },
                                    new cCreateJournalEntryInfo.Parameter()
                                    {
                                        Name = "ExecutionTime",
                                        Value = entry.ExecutionTime
                                    },
                                    new cCreateJournalEntryInfo.Parameter()
                                    {
                                        Name = "AffectedDeviceName",
                                        Value = entry.AffectedDeviceName
                                    },}
                        };
                        break;
                    default:
                        break;
                }
                if (_entry != null)
                {
                    await CreateM42JournalAsync(_entry, requestInfo, logDeep + 1, token);
                    if (token.IsCancellationRequested) return;
                }
            }
        }

        public async Task<string> UpdateM42TicketAsync(cApiM42Ticket ticketInfo, cF4sdWebRequestInfo requestInfo, int LogDeep, CancellationToken token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); }
            var _startTime = DateTime.UtcNow;

            try
            {
                var _webClient = await GetUserWebClient(requestInfo, LogDeep + 1, token);
                if (token.IsCancellationRequested || _webClient == null) return null;
                string activityType = "SPSActivityTypeIncident";

                if (ticketInfo.AdditionalValues.TryGetValue("ActivityType", out object value) && value is string stringValue)
                {
                    activityType = stringValue;
                }
                var url = string.Format(constUrlObjectsUpdateObject, activityType, false);
                var ticketID = (Guid)ticketInfo.Ticket;
                var ticket = await GetObjectAsync(activityType, ticketID, false, requestInfo, LogDeep + 1, token);

                if (ticketInfo.Quickcall.HasValue && ticketInfo.Quickcall.Value != Guid.Empty)
                {
                    ticket["SPSActivityClassIncident"]["Quickcall"] = ticketInfo.Quickcall.Value;
                }

                if (ticketInfo.Category.HasValue && ticketInfo.Category.Value != Guid.Empty)
                {
                    ticket["SPSActivityClassIncident"]["Category"] = ticketInfo.Category.Value;
                }
                if (ticketInfo.AffectedService.HasValue && ticketInfo.AffectedService.Value == Guid.Empty)
                {
                    ticket["SPSActivityClassBase"]["Service"] = null;
                }
                else if (ticketInfo.AffectedService.HasValue && ticketInfo.AffectedService.Value != Guid.Empty)
                    ticket["SPSActivityClassBase"]["Service"] = ticketInfo.AffectedService.Value;

                var createAssetNotFoundJournalEntry = false;
                if (ticketInfo.AffectedAsset.Equals("RemoveAffectedAssetEntry", StringComparison.InvariantCultureIgnoreCase))
                {
                    ticket["SPSActivityClassIncident"]["Asset"] = null;
                }
                else if (!string.IsNullOrEmpty(ticketInfo.AffectedAsset))
                {
                    var assetNotFound = true;
                    Guid? assetId = null;
                    var assetFragments = await GetFragmentListAsync(new cDataFragmentsParams()
                    {
                        Columns = "ID",
                        Ddname = "SPSAssetClassBase",
                        Pagenumber = 0,
                        Pagesize = 10,
                        Where = string.Format("T(SPSCommonClassbase).State = 710 AND T(SPSComputerClassBase).Name = '{0}'", ticketInfo.AffectedAsset)
                    }, requestInfo, LogDeep + 1, token);
                    if (token.IsCancellationRequested) return null;


                    if (assetFragments == null || assetFragments.Count == 0)
                    {
                        assetNotFound = true;
                    }
                    else
                    {
                        var idAsString = (string)assetFragments?[0]?.ID;
                        if (Guid.TryParse(idAsString, out Guid assetIdTemp))
                        {
                            assetId = assetIdTemp;
                            assetNotFound = false;
                        }
                        else
                        {
                            assetNotFound = true;
                        }
                    }
                    if (assetNotFound)
                    {
                        createAssetNotFoundJournalEntry = true;
                    }
                    if (assetId != null)
                    {
                        ticket["SPSActivityClassBase"]["AffectedAsset"] = assetId;
                    }

                }

                ticket["SPSActivityClassBase"]["Subject"] = ticketInfo.Summary;

                var _jsonContent = JsonConvert.SerializeObject(ticket, Formatting.None,
                    new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
                var _content = new StringContent(_jsonContent, Encoding.UTF8, "application/json");
                var response = await _webClient.HttpEnh.PutAsync(url, _content, token);
                if (token.IsCancellationRequested) return null;

                if (response.IsSuccessStatusCode)
                {
                    await AddWorkingTimesAsync(ticketID, ticketInfo.workTimes, requestInfo, LogDeep + 1, token);

                    cCreateJournalEntryInfo _entry = null;

                    if (createAssetNotFoundJournalEntry)
                    {
                        string assetNotFoundText = "";
                        if (ticketInfo.AdditionalValues.TryGetValue("Dialog.CloseCase.AssetNotFoundInMatrix42", out object valueObj))
                        {
                            assetNotFoundText = valueObj as string;
                        }
                        if (!string.IsNullOrEmpty(assetNotFoundText))
                        {
                            _entry = new cCreateJournalEntryInfo()
                            {
                                Comments = string.Format(assetNotFoundText, ticketInfo.AffectedAsset),
                                ObjectId = ticketID,
                                EntryType = 0
                            };
                            await CreateM42JournalAsync(_entry, requestInfo, LogDeep + 1, token);
                        }
                    }

                    // Case Notes as Journal Entry
                    if (!string.IsNullOrEmpty(ticketInfo.DescriptionHtml))
                    {
                        _entry = new cCreateJournalEntryInfo()
                        {
                            Comments = ticketInfo.DescriptionHtml,
                            ObjectId = ticketID,
                            EntryType = 20603,
                        };
                        await CreateM42JournalAsync(_entry, requestInfo, LogDeep + 1, token);
                        if (token.IsCancellationRequested) return null;
                    }

                    // CopyTemplates as Journal Entry
                    if (!string.IsNullOrEmpty(ticketInfo.CopyTemplates))
                    {
                        _entry = new cCreateJournalEntryInfo()
                        {
                            Comments = ticketInfo.CopyTemplates,
                            ObjectId = ticketID,
                            EntryType = 20604
                        };
                        await CreateM42JournalAsync(_entry, requestInfo, LogDeep + 1, token);
                        if (token.IsCancellationRequested) return null;
                    }
                    if (ticketInfo.SupportCaseProtocollEntries != null)
                    {
                        foreach (var protocollEntry in ticketInfo.SupportCaseProtocollEntries)
                        {
                            if (!(protocollEntry is QuickActionProtocollEntry entry))
                                continue;

                            switch (entry.ResultCode)
                            {
                                case -1:
                                case 1:
                                case 0:
                                    var entryType = entry.WasRunningOnAffectedDevice ? 20602 : 20601;
                                    _entry = new cCreateJournalEntryInfo()
                                    {
                                        Comments = "",
                                        ObjectId = ticketID,
                                        EntryType = entryType,
                                        Parameters = new List<cCreateJournalEntryInfo.Parameter>() { new cCreateJournalEntryInfo.Parameter()
                                    {
                                        Name = "result",
                                        Value = entry.HtmlContent ?? entry.MeasureValues ?? entry.ErrorMessage
                                    },
                                    new cCreateJournalEntryInfo.Parameter()
                                    {
                                        Name = "quickActionName",
                                        Value = entry.Name
                                    },
                                    new cCreateJournalEntryInfo.Parameter()
                                    {
                                        Name = "QuickActionId",
                                        Value = entry.Id
                                    },
                                    new cCreateJournalEntryInfo.Parameter()
                                    {
                                        Name = "quickActionExecutionType",
                                        Value = entry.ExecutionTypeId
                                    },
                                    new cCreateJournalEntryInfo.Parameter()
                                    {
                                        Name = "ExecutionTime",
                                        Value = entry.Time
                                    },
                                    new cCreateJournalEntryInfo.Parameter()
                                    {
                                        Name = "AffectedDeviceName",
                                        Value = entry.AffectedDeviceName
                                    },}
                                    };
                                    break;
                                default:
                                    break;
                            }
                            if (_entry != null)
                            {
                                await CreateM42JournalAsync(_entry, requestInfo, LogDeep + 1, token);
                                if (token.IsCancellationRequested) return null;
                            }
                        }
                    }
                    await UpdateM42TicketUnreadAsync(ticketInfo, requestInfo, LogDeep + 1, token);
                    return string.Empty;
                }
            }
            catch (Exception e)
            {
                LogException(e);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }

            return null;
        }

        public async Task<string> UpdateM42TicketUnreadAsync(cApiM42Ticket ticketInfo, cF4sdWebRequestInfo requestInfo, int LogDeep, CancellationToken token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); }
            var _startTime = DateTime.UtcNow;

            try
            {
                var _webClient = await GetUserWebClient(requestInfo, LogDeep + 1, token);
                if (token.IsCancellationRequested || _webClient == null) return null;
                string activityType = "SPSActivityTypeIncident";

                if (ticketInfo.AdditionalValues.TryGetValue("ActivityType", out object value) && value is string stringValue)
                {
                    activityType = stringValue;
                }
                var url = string.Format(constUrlObjectsUpdateObject, activityType, false);
                var ticketID = (Guid)ticketInfo.Ticket;
                var ticket = await GetObjectAsync(activityType, ticketID, false, requestInfo, LogDeep + 1, token);
                if (ticket != null)
                {
                    ticket["SPSActivityClassBase"]["NewInformationReceived"] = 0;
                    var _jsonContent = JsonConvert.SerializeObject(ticket, Formatting.None,
                                       new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
                    var _content = new StringContent(_jsonContent, Encoding.UTF8, "application/json");
                    var response = await _webClient.HttpEnh.PutAsync(url, _content, token);
                    if (token.IsCancellationRequested) return null;

                    if (response.IsSuccessStatusCode)
                    {
                        await Collector.ReportTicketAsync(ticketInfo, requestInfo, LogDeep + 1, token);
                        return string.Empty;
                    }
                }


            }
            catch (Exception e)
            {
                LogException(e);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }

            return null;
        }
        public async Task<string> CloseM42TicketAsync(cApiM42Ticket ticketInfo, cF4sdWebRequestInfo requestInfo, int LogDeep, CancellationToken token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); }
            var _startTime = DateTime.UtcNow;

            try
            {
                var _webClient = await GetUserWebClient(requestInfo, LogDeep + 1, token);
                if (token.IsCancellationRequested || _webClient == null) return null;

                var ticketId = Guid.Empty;
                if (ticketInfo.Ticket == null || ticketInfo.Ticket.HasValue && ticketInfo.Ticket.Equals(Guid.Empty))
                {

                    var res = await CreateM42TicketAsync(ticketInfo, requestInfo, LogDeep + 1, token);
                    if (token.IsCancellationRequested) return null;

                    if (!string.IsNullOrEmpty(res))
                    {
                        Guid.TryParse(res, out ticketId);
                    }
                }
                else
                {
                    await UpdateM42TicketAsync(ticketInfo, requestInfo, LogDeep + 1, token);
                    if (token.IsCancellationRequested) return null;

                    ticketId = (Guid)ticketInfo.Ticket;
                }

                if ((!ticketInfo.Ticket.HasValue || ticketInfo.Ticket.Value == Guid.Empty) && ticketId != Guid.Empty)
                {
                    ticketInfo.Ticket = ticketId;
                }

                var isTicket = await ResolveIsTicketAsync(ticketInfo, ticketId, requestInfo, LogDeep + 1, token);
                if (token.IsCancellationRequested) return null;

                if (isTicket)
                {
                    await TransformM42TicketAsync(ticketInfo, requestInfo, LogDeep + 1, token);
                }

                var url = constUrlTicketClose;
                var ticket = new cApiM42TicketClosureInfo()
                {
                    ObjectIds = new List<Guid>() { ticketId },
                    Comments = ticketInfo.SolutionHtml,
                    ErrorType = (int)ticketInfo.ErrorType == -1 ? null : ticketInfo.ErrorType,
                    Reason = 408,
                };

                var _jsonContent = JsonConvert.SerializeObject(ticket, Formatting.None,
                    new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
                var _content = new StringContent(_jsonContent, Encoding.UTF8, "application/json");

                var response = await _webClient.HttpEnh.PostAsync(url, _content, token);
                if (token.IsCancellationRequested) return null;

                if (response.IsSuccessStatusCode)
                {
                    ticketInfo.Ticket = ticketInfo.Ticket != null && ticketInfo.Ticket != Guid.Empty ? ticketInfo.Ticket : ticketId;
                    await UpdateM42TicketUnreadAsync(ticketInfo, requestInfo, LogDeep + 1, token);
                    return string.Empty;
                }
            }
            catch (Exception e)
            {
                LogException(e);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }

            return null;
        }
        public async Task<bool> AddWorkingTimesAsync(Guid TicketEOID, Dictionary<string, object> workTimes, cF4sdWebRequestInfo requestInfo, int LogDeep, CancellationToken token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); }
            var _startTime = DateTime.UtcNow;

            try
            {
                if (Collector == null) return false;
                var globalConfig = Collector.GetGlobalConfig();
                var timeTrackingDisabled = globalConfig.TicketConfiguration.DisableAutomaticTimeTracking;
                if (timeTrackingDisabled)
                    return false;

                var _webClient = await GetUserWebClient(requestInfo, LogDeep + 1, token);
                if (token.IsCancellationRequested || _webClient == null) return false;


                if (token.IsCancellationRequested) return false;
                var fragments = await GetFragmentListAsync(new cDataFragmentsParams()
                {
                    Columns = "ID, TypeID",
                    Ddname = "SPSCommonClassBase",
                    Pagenumber = 0,
                    Pagesize = 10,
                    Where = string.Format("[Expression-ObjectId] = '{0}'", TicketEOID)
                }, requestInfo, LogDeep + 1, token);
                if (token.IsCancellationRequested) return false;
                var idAsString = "";
                Guid? typeId = null;
                if (fragments != null)
                {
                    idAsString = (string)fragments?[0]?.ID;
                    if (Guid.TryParse(idAsString, out Guid typeIdTemp))
                    {
                        typeId = typeIdTemp;
                    }
                }

                string description = default;
                DateTime startTime = default;
                DateTime endTime = default;
                TimeSpan bruttoWorkingTime = default;
                int nettoWorkingTime = 0;
                TimeSpan totalPausedTime = default;


                if (workTimes.TryGetValue("Description", out object tempDescription) && tempDescription is string)
                {
                    description = (string)tempDescription;
                }

                if (workTimes.TryGetValue("StartTime", out object tempStartTime) && tempStartTime is DateTime)
                {
                    startTime = (DateTime)tempStartTime;
                }


                if (workTimes.TryGetValue("EndTime", out object tempEndTime) && tempEndTime is DateTime)
                {
                    endTime = (DateTime)tempEndTime;
                }

                if (workTimes.TryGetValue("BruttoWorkingTime", out object tempBruttoWorkingTime) && tempBruttoWorkingTime is TimeSpan)
                {
                    bruttoWorkingTime = (TimeSpan)tempBruttoWorkingTime;
                }

                if (workTimes.TryGetValue("NettoWorkingTime", out object tempNettoWorkingTime) && tempNettoWorkingTime is double)
                {
                    nettoWorkingTime = (int)Math.Round((double)tempNettoWorkingTime / 60);
                }

                if (workTimes.TryGetValue("TotalPausedTime", out object tempTotalPausedTime) && tempTotalPausedTime is TimeSpan)
                {
                    totalPausedTime = (TimeSpan)tempTotalPausedTime;
                }




                var requestBody = new cApiM42TicketAddWorkingTimesInfo()
                {
                    ObjectIds = new List<Guid>() { TicketEOID },
                    Begin = startTime,
                    End = endTime,
                    TypeId = typeId,
                    ActivityType = 4,
                    Minutes = nettoWorkingTime <= 0 ? 1 : nettoWorkingTime,
                    Description = description
                };

                var url = constUrlActivityTrackWorkingTime;

                var _jsonContent = JsonConvert.SerializeObject(requestBody, Formatting.None,
                        new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
                var _content = new StringContent(_jsonContent, Encoding.UTF8, "application/json");

                var response = await _webClient.HttpEnh.PostAsync(url, _content, token);
                if (token.IsCancellationRequested) return false;

                if (response.IsSuccessStatusCode)
                {
                    return true;
                }

            }
            catch (Exception e)
            {
                LogException(e);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }

            return false;
        }
        public async Task<string> TransformM42TicketAsync(cApiM42Ticket ticketInfo, cF4sdWebRequestInfo requestInfo, int LogDeep, CancellationToken token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); }
            var _startTime = DateTime.UtcNow;

            try
            {
                var _webClient = await GetUserWebClient(requestInfo, LogDeep + 1, token);
                if (token.IsCancellationRequested || _webClient == null) return null;

                var ticketId = Guid.Empty;
                if (ticketInfo.Ticket == null || ticketInfo.Ticket.HasValue && ticketInfo.Ticket.Equals(Guid.Empty))
                    return null;
                else
                    ticketId = ticketInfo.Ticket.Value;
                var url = constUrlTicketTransform;

                var ticket = new cM42TransformTicketInfo()
                {
                    ObjectIds = new List<Guid>() { ticketId },
                    SourceTypeName = "SPSActivityTypeTicket",
                    TargetTypeName = "SPSActivityTypeIncident"
                };


                var _jsonContent = JsonConvert.SerializeObject(ticket, Formatting.None,
                    new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
                var _content = new StringContent(_jsonContent, Encoding.UTF8, "application/json");

                var response = await _webClient.HttpEnh.PostAsync(url, _content, token);
                if (token.IsCancellationRequested) return null;

                if (response.IsSuccessStatusCode)
                {
                    await Collector.ReportTicketAsync(ticketInfo, requestInfo, LogDeep + 1, token);
                    return string.Empty;
                }
            }
            catch (Exception e)
            {
                LogException(e);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }

            return null;
        }

        private async Task<bool> IsTicketActivityAsync(Guid ticketId, cF4sdWebRequestInfo requestInfo, int logDeep, CancellationToken token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(logDeep, CM, requestInfo.id, requestInfo.created); }
            var _startTime = DateTime.UtcNow;

            try
            {
                if (ticketId == Guid.Empty)
                    return false;

                var fragments = await GetFragmentListAsync(new cDataFragmentsParams()
                {
                    Columns = "TypeID",
                    Ddname = "SPSCommonClassBase",
                    Pagenumber = 0,
                    Pagesize = 1,
                    Where = string.Format("[Expression-ObjectId] = '{0}'", ticketId),
                    Sort = "TypeID"
                }, requestInfo, logDeep + 1, token);

                if (token.IsCancellationRequested || fragments == null || fragments.Count == 0)
                    return false;

                var activityTypeRawValue = fragments[0]?.TypeID;
                Guid activityTypeId = Guid.Empty;
                if (activityTypeRawValue is Guid guidValue)
                {
                    activityTypeId = guidValue;
                }
                else
                {
                    var activityTypeText = Convert.ToString(activityTypeRawValue);
                    Guid.TryParse(activityTypeText, out activityTypeId);
                }

                if (activityTypeId == Guid.Empty)
                    return false;

                return activityTypeId == constActivityTypeTicketTypeId;
            }
            catch (Exception e)
            {
                LogException(e);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(logDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }

            return false;
        }

        private async Task<bool> ResolveIsTicketAsync(cApiM42Ticket ticketInfo, Guid ticketId, cF4sdWebRequestInfo requestInfo, int logDeep, CancellationToken token)
        {
            if (ticketInfo != null && ticketInfo.AdditionalValues != null)
            {
                object additionalValue;
                if (ticketInfo.AdditionalValues.TryGetValue("IsTicket", out additionalValue) && additionalValue is bool boolValue && boolValue)
                    return true;

                if (ticketInfo.AdditionalValues.TryGetValue("TypeID", out additionalValue) && IsTicketActivityTypeValue(additionalValue))
                    return true;

                if (ticketInfo.AdditionalValues.TryGetValue("ActivityType", out additionalValue) && IsTicketActivityTypeValue(additionalValue))
                    return true;
            }

            if (ticketId == Guid.Empty)
                return false;

            return await IsTicketActivityAsync(ticketId, requestInfo, logDeep, token);
        }

        private static bool IsTicketActivityTypeValue(object activityTypeValue)
        {
            if (activityTypeValue == null)
                return false;

            if (activityTypeValue is Guid guidValue)
                return guidValue == constActivityTypeTicketTypeId;

            var activityTypeText = activityTypeValue as string;
            if (string.IsNullOrWhiteSpace(activityTypeText))
                activityTypeText = Convert.ToString(activityTypeValue);

            if (string.IsNullOrWhiteSpace(activityTypeText))
                return false;

            Guid activityTypeGuid;
            if (Guid.TryParse(activityTypeText, out activityTypeGuid))
                return activityTypeGuid == constActivityTypeTicketTypeId;

            return string.Equals(activityTypeText, constActivityTypeTicket, StringComparison.OrdinalIgnoreCase);
        }
        public async Task<dynamic> GetQuickCallTemplateInfoAsync(Guid quickCallId, cF4sdWebRequestInfo requestInfo, int LogDeep, CancellationToken token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); }
            var _startTime = DateTime.UtcNow;

            try
            {
                var _webClient = await GetUserWebClient(requestInfo, LogDeep + 1, token);
                if (token.IsCancellationRequested || _webClient == null) return null;


                if (quickCallId.Equals(Guid.Empty))
                    return null;

                var url = string.Format(constUrlActivityGetQuickCallTemplateInfo, quickCallId);

                var response = await _webClient.HttpEnh.GetAsync(url, token);
                if (token.IsCancellationRequested) return null;

                if (response.IsSuccessStatusCode)
                {
                    var json = await response.Content.ReadAsStringAsync();
                    return JsonConvert.DeserializeObject(json);
                }
            }
            catch (Exception e)
            {
                LogException(e);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }

            return null;
        }
        public async Task<string> ReopenM42TicketAsync(cApiM42Ticket ticketInfo, cF4sdWebRequestInfo requestInfo, int LogDeep, CancellationToken token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); }
            var _startTime = DateTime.UtcNow;

            try
            {
                var _webClient = await GetUserWebClient(requestInfo, LogDeep + 1, token);
                if (token.IsCancellationRequested || _webClient == null) return null;

                var ticketId = Guid.Empty;
                if (ticketInfo.Ticket == null || ticketInfo.Ticket.HasValue && ticketInfo.Ticket.Equals(Guid.Empty))
                    return null;
                else
                    ticketId = ticketInfo.Ticket.Value;
                var url = constUrlActivityReopen;
                string ReopenReasonText = "";
                if (ticketInfo.AdditionalValues.TryGetValue("Dialog.CloseCase.ReopenReasonText", out object valueObj))
                {
                    ReopenReasonText = valueObj as string;
                }
                var ticket = new cM42ReopenTicketInfo()
                {
                    ObjectIds = new List<Guid>() { ticketId },
                    Reason = ReopenReasonText,
                };


                var _jsonContent = JsonConvert.SerializeObject(ticket, Formatting.None,
                    new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
                var _content = new StringContent(_jsonContent, Encoding.UTF8, "application/json");

                var response = await _webClient.HttpEnh.PostAsync(url, _content, token);
                if (token.IsCancellationRequested) return null;

                if (response.IsSuccessStatusCode)
                {
                    //await Collector.ReportTicketAsync(ticketInfo, requestInfo, LogDeep + 1, token).ConfigureAwait(false);
                    return string.Empty;
                }
            }
            catch (Exception e)
            {
                LogException(e);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }

            return null;
        }
        public async Task<string> PauseM42TicketAsync(cApiM42Ticket ticketInfo, cF4sdWebRequestInfo requestInfo, int LogDeep, CancellationToken token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); }
            var _startTime = DateTime.UtcNow;

            try
            {
                var _webClient = await GetUserWebClient(requestInfo, LogDeep + 1, token);
                if (token.IsCancellationRequested || _webClient == null) return null;

                var ticketId = Guid.Empty;
                if (ticketInfo.Ticket == null || ticketInfo.Ticket.HasValue && ticketInfo.Ticket.Equals(Guid.Empty))
                {

                    var res = await CreateM42TicketAsync(ticketInfo, requestInfo, LogDeep + 1, token);
                    if (token.IsCancellationRequested) return null;

                    if (!string.IsNullOrEmpty(res))
                    {
                        Guid.TryParse(res, out ticketId);
                    }
                }
                else
                {
                    await UpdateM42TicketAsync(ticketInfo, requestInfo, LogDeep + 1, token);
                    if (token.IsCancellationRequested) return null;

                    ticketId = (Guid)ticketInfo.Ticket;
                }

                if ((!ticketInfo.Ticket.HasValue || ticketInfo.Ticket.Value == Guid.Empty) && ticketId != Guid.Empty)
                {
                    ticketInfo.Ticket = ticketId;
                }

                var isTicket = await ResolveIsTicketAsync(ticketInfo, ticketId, requestInfo, LogDeep + 1, token);
                if (token.IsCancellationRequested) return null;

                if (isTicket)
                {
                    await TransformM42TicketAsync(ticketInfo, requestInfo, LogDeep + 1, token);
                }

                var url = constUrlActivityPause;
                var ticket = new cApiM42TicketPauseInfo()
                {
                    ObjectIds = new List<Guid>() { ticketId },
                    Comments = ticketInfo.Comment,
                    ReminderDate = (DateTime)ticketInfo.ReminderDate,
                    NotEscalateWhilePaused = (bool)ticketInfo.DoNotEscalateWhilePaused,
                    Reason = (int)ticketInfo.Reason
                };


                var _jsonContent = JsonConvert.SerializeObject(ticket, Formatting.None,
                    new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
                var _content = new StringContent(_jsonContent, Encoding.UTF8, "application/json");

                var response = await _webClient.HttpEnh.PostAsync(url, _content, token);
                if (token.IsCancellationRequested) return null;

                if (response.IsSuccessStatusCode)
                {
                    ticketInfo.Ticket = ticketInfo.Ticket != null && ticketInfo.Ticket != Guid.Empty ? ticketInfo.Ticket : ticketId;
                    await UpdateM42TicketUnreadAsync(ticketInfo, requestInfo, LogDeep + 1, token);
                    return string.Empty;
                }
            }
            catch (Exception e)
            {
                LogException(e);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }

            return null;
        }
        public async Task<string> ForwardM42TicketAsync(cApiM42Ticket ticketInfo, cF4sdWebRequestInfo requestInfo, int LogDeep, CancellationToken token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); }
            var _startTime = DateTime.UtcNow;

            try
            {
                var _webClient = await GetUserWebClient(requestInfo, LogDeep + 1, token);
                if (token.IsCancellationRequested || _webClient == null) return null;

                var ticketId = Guid.Empty;
                if (ticketInfo.Ticket == null || ticketInfo.Ticket.HasValue && ticketInfo.Ticket.Equals(Guid.Empty))
                {

                    var res = await CreateM42TicketAsync(ticketInfo, requestInfo, LogDeep + 1, token);
                    if (token.IsCancellationRequested) return null;

                    if (!string.IsNullOrEmpty(res))
                    {
                        Guid.TryParse(res, out ticketId);
                    }
                }
                else
                {
                    await UpdateM42TicketAsync(ticketInfo, requestInfo, LogDeep + 1, token);
                    if (token.IsCancellationRequested) return null;

                    ticketId = (Guid)ticketInfo.Ticket;
                }
                var url = constUrlActivityForward;


                var ticket = new cApiM42TicketForwardInfo()
                {
                    Tickets = new List<cApiM42TicketForwardInfo.cTicket>() { new cApiM42TicketForwardInfo.cTicket() {
                        Item1 = "SPSActivityTypeIncident",
                        Item2 = ticketId
                    }},
                    Comments = ticketInfo.Comment,
                    RoleId = ticketInfo.ResponsibleRole,
                    UserId = ticketInfo.ResponsiblePerson,
                };


                var _jsonContent = JsonConvert.SerializeObject(ticket, Formatting.None,
                    new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
                var _content = new StringContent(_jsonContent, Encoding.UTF8, "application/json");

                var response = await _webClient.HttpEnh.PostAsync(url, _content, token);
                if (token.IsCancellationRequested) return null;

                if (response.IsSuccessStatusCode)
                {
                    ticketInfo.Ticket = ticketInfo.Ticket != null && ticketInfo.Ticket != Guid.Empty ? ticketInfo.Ticket : ticketId;
                    await UpdateM42TicketUnreadAsync(ticketInfo, requestInfo, LogDeep + 1, token);
                    return string.Empty;
                }
            }
            catch (Exception e)
            {
                LogException(e);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }

            return null;
        }

        public async Task<String> CreateM42JournalAsync(cCreateJournalEntryInfo journalEntryInfo, cF4sdWebRequestInfo requestInfo, int LogDeep, CancellationToken token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); }
            var _startTime = DateTime.UtcNow;

            try
            {
                var _webClient = await GetUserWebClient(requestInfo, LogDeep + 1, token);
                if (token.IsCancellationRequested || _webClient == null) return null;

                var _jsonContent = JsonConvert.SerializeObject(journalEntryInfo, Formatting.None,
                   new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
                var _content = new StringContent(_jsonContent, Encoding.UTF8, "application/json");

                var response = await _webClient.HttpEnh.PostAsync(constUrlCreateJournalEntry, _content, token);
                if (token.IsCancellationRequested) return null;

                if (response.IsSuccessStatusCode)
                {
                    var json = await response.Content.ReadAsStringAsync();
                    if (token.IsCancellationRequested) return null;
                    return json;
                }
            }
            catch (Exception e)
            {
                LogException(e);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }
            return null;
        }

        public static string GetFragmentUrlWithParameters(cDataFragmentsParams parameters)
        {
            return string.Format(constUrlFragmentsGetList,
                parameters.Ddname,
                Uri.EscapeDataString(parameters.Where), // Ensure proper URI encoding
                parameters.Columns,
                parameters.Pagesize,
                parameters.Pagenumber,
                parameters.Sort);
        }


        public async Task CleanupApiTokensAsync(CancellationToken Token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }

            if (WebClient is null)
                return;

            try
            {
                var _strFilter = (DateTime.UtcNow - TimeSpan.FromHours(24)).ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss");
                _strFilter = string.Format("Name like 'F4SD_%' and ValidUntil < '{0}'", _strFilter);
                var _strListToken = await WebClient.GetFragmentListAsync("PLSTSApiTokenBase", Token, Where: _strFilter, Columns: "ID,Name");

                if (Token.IsCancellationRequested)
                    return;

                if (!string.IsNullOrEmpty(_strListToken))
                {
                    // remove all existing tokens with corresponding name
                    var lstIds = JsonConvert.DeserializeObject<List<cM42IdResult>>(_strListToken);
                    foreach (var objId in lstIds)
                    {
                        if (string.IsNullOrEmpty(objId.Name))
                            continue;
                        if (objId.Name.Length < 37)
                            continue;
                        var _strGuid = objId.Name.Substring(objId.Name.Length - 37, 37);
                        if (!_strGuid.StartsWith("_"))
                            continue;
                        _strGuid = _strGuid.Remove(0, 1);
                        if (!Guid.TryParse(_strGuid, out _))
                            continue;

                        var _delRequest = new Dictionary<Guid, string>()
                        {
                            { objId.Id, "PLSTSApiTokenType" }
                        };

                        await WebClient.DeleteObjectsAsync("F4SD Cockpit", _delRequest, Token);

                        if (Token.IsCancellationRequested)
                            return;
                    }
                }
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (CM != null) LogMethodEnd(CM);
            }
        }

        public override async Task<cF4SDAdditionalUserInfo> GetAdditionalUserInfo(enumAdditionalAuthentication Type, cF4sdWebRequestInfo RequestInfo, int LogDeep, CancellationToken Token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            try
            {
                if (Type != enumAdditionalAuthentication.M42WinLogon)
                    return null;

                if (RequestInfo?.userInfo?.Id is null)
                    return null;

                var _uc = await Collector.TokenCache.GetAsync(RequestInfo.userInfo.Id, cF4SDTokenRegistration.enumTokenType.M42Bearer, RequestInfo, LogDeep + 1);
                if (_uc == null) return null;

                var _m42 = new cM42WebClient(Collector.InfrastructureConfig.M42Wpm.Server, UseSSL: true);
                _m42.SetBearerToken(_uc.secret);
                var _m42Reason = await _m42.Open();
                if (_m42Reason == eReason.ok && _m42.BearerToken != null)
                {
                    var m42UserInfo = await _m42.GetUserInfoAsync();
                    if (m42UserInfo?.User != null)
                    {
                        var _retVal = new cF4SDAdditionalUserInfo()
                        {
                            internalID = m42UserInfo.User.Id.ToString(),
                            Name = m42UserInfo.User.LastName + ", " + m42UserInfo.User.FirstName,
                            Type = enumAdditionalAuthentication.M42WinLogon,
                            ValidUntil = _uc.validUntil
                        };

                        return _retVal;
                    }
                }
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (CM != null) LogMethodEnd(CM);
            }

            return null;
        }


        public async Task<dynamic> GetFragmentListAsync(cDataFragmentsParams fragmentsParams, cF4sdWebRequestInfo requestInfo, int LogDeep, CancellationToken token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); }
            var _startTime = DateTime.UtcNow;

            try
            {
                var _webClient = await GetUserWebClient(requestInfo, LogDeep + 1, token);
                if (token.IsCancellationRequested || _webClient == null)
                    return null;

                var response = await _webClient.HttpEnh.GetAsync(GetFragmentUrlWithParameters(fragmentsParams), token);
                if (token.IsCancellationRequested) return null;

                if (response.IsSuccessStatusCode)
                {
                    var json = await response.Content.ReadAsStringAsync();
                    return JsonConvert.DeserializeObject(json);
                }
            }
            catch (Exception e)
            {
                LogException(e);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }
            return null;
        }

        public async Task<dynamic> GetObjectAsync(string ci, Guid id, bool full, cF4sdWebRequestInfo requestInfo, int LogDeep, CancellationToken token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); }
            var _startTime = DateTime.UtcNow;

            try
            {
                var _webClient = await GetUserWebClient(requestInfo, LogDeep + 1, token);
                if (token.IsCancellationRequested || _webClient == null)
                    return null;

                var url = string.Format(constUrlObjectsGetObject, ci, id, full);
                var response = await _webClient.HttpEnh.GetAsync(url, token);
                if (token.IsCancellationRequested) return null;

                if (response.IsSuccessStatusCode)
                {
                    var json = await response.Content.ReadAsStringAsync();
                    if (token.IsCancellationRequested) return null;
                    return JsonConvert.DeserializeObject(json);
                }
            }
            catch (Exception e)
            {
                LogException(e);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }
            return null;
        }


        static string RemoveEmptyQueryParameters(string url)
        {
            var uri = new Uri("http://dummy.com/" + url);
            var queryParts = HttpUtility.ParseQueryString(uri.Query);

            var newQuery = "";
            foreach (string key in queryParts.AllKeys)
            {
                if (!string.IsNullOrEmpty(queryParts[key]))
                {
                    newQuery += string.IsNullOrEmpty(newQuery) ? "?" : "&";
                    newQuery += $"{key}={queryParts[key]}";
                }
            }

            string cleanedUrl = uri.AbsolutePath.Substring(1) + newQuery;
            return cleanedUrl;
        }

        public HashSet<enumFasdInformationClass> GetSupportedInformationClasses() => new HashSet<enumFasdInformationClass>() { enumFasdInformationClass.Ticket };

        public async Task<cF4sdStagedSearchResultRelations> GetRelationsAsync(IEnumerable<cF4sdConnectorIds> ids, enumFasdInformationClass informationClass, int age, CancellationToken token = default)
        {
            try
            {
                List<cF4sdApiSearchResultRelation> relations = await getTicketUsage(ids, token, null, 0);
                return new cF4sdStagedSearchResultRelations() { Relations = relations };
            }
            catch (Exception ex)
            {
                LogException(ex);
            }

            return null;
        }

        public async override Task<int> ValidateTokenInfo(cDataHistoryCollectorTokenCache.cTokenInfo tokenInfo)
        {
            if (Guid.TryParseExact(tokenInfo?.IdRemote, "D", out Guid remoteId))
                return 0;

            if (tokenInfo.type == cF4SDTokenRegistration.enumTokenType.M42Bearer)
            {
                using (var client = new cM42WebClient(Collector.InfrastructureConfig.M42Wpm.Server, null))
                {
                    client.SetBearerToken(tokenInfo.secret);
                    var _reason = await client.Open();
                    if (_reason == eReason.ok)
                    {
                        var userInfo = await client.GetUserInfoAsync();
                        if (userInfo?.User != null)
                        {
                            tokenInfo.IdRemote = userInfo.User.Id.ToString();
                            return 1;
                        }
                    }
                }
            }

            if (tokenInfo.type == cF4SDTokenRegistration.enumTokenType.M42Api)
            {
                var _ti = tokenInfo.GetNewInstance();
                using (var client = new cM42WebClient(Collector.InfrastructureConfig.M42Wpm.Server, null))
                {
                    var _reason = await client.LogonWithApiKeyAsync(_ti.secret);
                    if (_reason == eReason.ok)
                    {
                        var userInfo = await client.GetUserInfoAsync();
                        if (userInfo?.User != null)
                        {
                            tokenInfo.IdRemote = userInfo.User.Id.ToString();
                            return 1;
                        }
                    }
                }
            }

            return -1;
        }

    }

    public class cDataFragmentsParams
    {
        public string Ddname { get; set; }
        public string Where { get; set; }
        public string Columns { get; set; }
        public int Pagesize { get; set; }
        public int Pagenumber { get; set; }
        public string Sort { get; set; }
    }

    public class cCreateJournalEntryInfo
    {
        public Guid ObjectId { get; set; }
        public Guid TypeId { get; set; }
        public bool Publish { get; set; }
        public string Comments { get; set; }
        public int EntryType { get; set; }
        public Guid Creator { get; set; }
        public bool VisibleInPortal { get; set; }
        public List<Guid> FileIds { get; set; }
        public List<Parameter> Parameters { get; set; }

        public class Parameter
        {
            public string Name { get; set; }
            public object Value { get; set; }
            public string Format { get; set; }
            public bool IsCurrency { get; set; }
        }
    }

    public class cM42TransformTicketInfo
    {
        public Guid? Category { get; set; } = null;
        public Guid? Sla { get; set; } = null;
        public Guid? RecipientRole { get; set; } = null;
        public string SourceTypeName { get; set; } = null;
        public string TargetTypeName { get; set; } = null;
        public List<Guid> ObjectIds { get; set; } = new List<Guid>();
        public bool? UpdateFavoriteView { get; set; } = null;
        public bool? RaiseCoRuTypeInsertEvent { get; set; } = null;
    }
    public class cM42ReopenTicketInfo
    {
        public string Reason { get; set; } = null;
        public List<Guid> ObjectIds { get; set; } = new List<Guid>();
    }
    public class cM42TicketInfo
    {
        public JournalEntryData EntryData { get; set; }
        public Guid? Asset { get; set; } = null;
        public Guid? AffectedAsset { get; set; } = null;
        public Guid? AffectedService { get; set; } = null;
        public string UserReachableAt { get; set; } = null;
        public int EntryBy { get; set; } = 0;
        public int Classification { get; set; } = 0;
        public Guid? QueueProfile { get; set; } = null;
        public Guid? Template { get; set; } = null;
        public Guid? Category { get; set; } = null;
        public string Subject { get; set; } = null;
        public string Description { get; set; } = null;
        public string DescriptionHTML { get; set; } = null;
        public string Solution { get; set; } = null;
        public string SolutionHTML { get; set; } = null;
        public int ActivityAction { get; set; } = 0;
        public int TaskType { get; set; } = 0;
        public int ErrorType { get; set; } = 0;
        public int State { get; set; } = 0;
        public int Priority { get; set; } = 0;
        public int Impact { get; set; } = 0;
        public int Urgency { get; set; } = 0;
        public Guid? User { get; set; } = null;
        public Guid? ResponsibleUser { get; set; } = null;
        public Guid? ResponsibleRole { get; set; } = null;
        public Guid? Service { get; set; } = null;
        public Guid? SLA { get; set; } = null;
        public string TicketNumber { get; set; } = null;
        public DateTime Deadline { get; set; } = DateTime.UtcNow;
        public bool NotifyResponsible { get; set; } = false;
        public int NotificationMode { get; set; } = 0;
        public bool CloseOnTasksCompletion { get; set; } = false;
        public Guid? WorkflowId { get; set; } = null;
        public List<Guid> RelatedUsers { get; set; } = new List<Guid>();
        public List<Guid> RelatedServices { get; set; } = new List<Guid>();
        public List<Guid> SimpleServiceForms { get; set; } = new List<Guid>();
        public List<Guid> ConfigurationItems { get; set; } = new List<Guid>();
        public List<Guid> RelatedTasks { get; set; } = new List<Guid>();
        public List<Guid> RelatedAssets { get; set; } = new List<Guid>();
        public List<Guid> RelatedIncidents { get; set; } = new List<Guid>();
        public List<Guid> RelatedProblems { get; set; } = new List<Guid>();
        public bool NewInformationReceived { get; set; } = false;
        public bool SkipSettingOwner { get; set; } = false;
        public bool RecalculateResponsibleRole { get; set; } = false;
        public List<ExtraProperty> ExtraProperties { get; set; } = new List<ExtraProperty>();
    }

    public class JournalEntryData
    {
        public Guid? ObjectId { get; set; } = null;
        public Guid? TypeId { get; set; } = null;
        public bool Publish { get; set; } = false;
        public string Comments { get; set; } = null;
        public int EntryType { get; set; } = 0;
        public Guid? Creator { get; set; } = null;
        public bool VisibleInPortal { get; set; } = false;
        public List<Parameter> Parameters { get; set; } = new List<Parameter>();
        public bool SkipRaiseCoRuEvent { get; set; } = false;
    }

    public class Parameter
    {
        public bool IsPortalMode { get; set; } = false;
        public bool IsExportMode { get; set; } = false;
        public string Name { get; set; } = null;
    }

    public class ExtraProperty
    {
        public string Name { get; set; } = null;
        public object Value { get; set; } = null;
    }
}

public class cM42DataQueryRelationInfo
{
    public int ClientTimeOffset { get; set; } = 0;
    public string EntityClassName { get; set; }
    public string EntityTypeName { get; set; }
    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
    public string DataQueryName { get; set; }
    public string AddedRelations { get; set; } = "";
    public string RemovedRelations { get; set; } = "";
    public string WhereExpression { get; set; } = "";
    public cM42QueryFilterGroup AdditionalFilter { get; set; } = new cM42QueryFilterGroup();
    public int pageSize { get; set; } = 20;
    public int page { get; set; } = 0;
    public bool totalCounted { get; set; } = false;
    public string search { get; set; } = "";
    public string orderBy { get; set; } = "";
    public string columns { get; set; }
}

public class cM42DataQueryRelationInfo2
{
    public int ClientTimeOffset { get; set; } = 0;
    public string EntityClassName { get; set; }
    public string EntityTypeName { get; set; }
    public string AddedRelations { get; set; } = "";
    public string RemovedRelations { get; set; } = "";
    public string WhereExpression { get; set; } = "";
    public cM42QueryFilterGroup AdditionalFilter { get; set; } = new cM42QueryFilterGroup();
    public int pageSize { get; set; } = 0;
    public int page { get; set; } = 0;
    public string search { get; set; } = "";
    public string orderBy { get; set; } = "";
    public string columns { get; set; }
    public string RelationName { get; set; }
    public Guid FragmentId { get; set; }
    public bool Archived { get; set; } = false;
    public string Filters { get; set; } = "";
}

public class cM42QueryFilterGroup
{
    public cM42QueryFilterCondition[] Conditions { get; set; } = new cM42QueryFilterCondition[0];
    public cM42QueryFilterGroup[] Groups { get; set; } = new cM42QueryFilterGroup[0];
    public int LogicalOperator { get; set; } = 1;
}


public class cM42QueryFilterCondition
{
    public int Operator { get; set; }
    public string Property { get; set; }
    public string[] Value { get; set; }
    public bool IsExpression { get; set; }
    public bool DenyWildcard { get; set; }
}

public class M42UserUserInfo
{
    public class M42User
    {
        public Guid Id { get; set; } = Guid.Empty;
        public Guid ObjectId { get; set; }
        public string Name { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
        public string Photo { get; set; }
        public string Phone { get; set; }
        public string Currency { get; set; }
        public string Locale { get; set; }
        public int LanguageId { get; set; }
        public bool IsAdmin { get; set; }
        public int State { get; set; }
        public string StateDisp { get; set; }
    }

    public class M42Role
    {
        public Guid Id { get; set; }
        public string Name { get; set; }

    }

    public M42User User { get; set; }
    public List<M42Role> Roles { get; set; }

}


