using ActiveDs;
using C4IT.FASD.Base;
using C4IT.HTTP;
using C4IT.Logging;
using C4IT.MsGraph;
using C4IT_DataHistoryProvider_Base.DataSources;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Data.Common;
using System.DirectoryServices.AccountManagement;
using System.DirectoryServices.ActiveDirectory;
using System.DirectoryServices.Protocols;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Security.Principal;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using static C4IT.Logging.cLogManager;

namespace C4IT.DataHistoryProvider
{
    public enum enumLdapPropertyType { ldapString, ldapTimeInterval, ldapSid, ldapPhone };

    public class cDataHistoryCollectorActiveDirectory : cDataHistoryCollectorModule, ISearchResultRelationProvider
    {
        public const string constConnectorName = "MS Active Directory, Entra ID & Intune connector";
        public const string constLicenseId = "B2D48023-56E8-44D5-AB77-BE1DCF4EA4EB";

        public const string constMsGraphUserList = "users?$top={0}&$count=true&$select=id,displayName,onPremisesSyncEnabled,onPremisesSecurityIdentifier&$filter=onPremisesSyncEnabled eq true";
        public const string constMsGraphUserEntry = "users/{0}";
        public const string constMsGraphUserDeviceEntry = "users/{0}";
        public const int constMsGraphUserPaging = 100;
        public const string constMsGraphDeviceList = "devices/?$top={0}&$select=id,deviceId,displayName,onPremisesSyncEnabled,onPremisesSecurityIdentifier&$filter=onPremisesSyncEnabled eq true";
        public const string constMsGraphDeviceEntry = "devices/{0}";
        public const int constMsGraphDevicePaging = 100;
        public const string constMsGraphManagedDeviceList = "deviceManagement/managedDevices?$top={0}&$select=id,azureActiveDirectoryDeviceId,deviceName,userPrincipalName,lastSyncDateTime&$filter=operatingSystem eq 'Windows'";
        public const string constMsGraphManagedMobileDeviceList = "users/{0}/managedDevices?$select=id,userId,deviceName,lastSyncDateTime&$filter=operatingSystem eq 'Android' or operatingSystem eq 'iOS' or operatingSystem eq 'iPadOS'";
        public const string constMsGraphManagedDeviceEntry = "deviceManagement/managedDevices/{0}?$select=id,complianceState,userPrincipalName,userDisplayName,managementState,lastSyncDateTime";
        public const string constMsGraphManagedDeviceAllEntry = "deviceManagement/managedDevices/{0}";
        public const string constMsGraphManagedDeviceSyncDevice = "deviceManagement/managedDevices/{0}/syncDevice";
        public const string constMsGraphManagedDeviceDetectedApps = "deviceManagement/managedDevices/{0}/detectedApps";
        public const string constMsGraphManagedDeviceCount = "users/{0}/managedDevices?$select=operatingSystem";
        public const string constMsGraphManagedUserDeviceEntry = "users/{0}/managedDevices?$select=id,deviceName,model,operatingSystem,manufacturer,serialNumber";
        public const string constMsGraphManagedDeviceChangeManagedStatus = "deviceManagement/managedDevices/{0}/{1}";
        public const string constMsGraphTest = "deviceManagement/managedDevices";
        public const string constMsGraphDeviceBitlockerRecoveryKey = "informationProtection/bitlocker/recoveryKeys?$filter=deviceId eq '{0}'";
        public const string constMsGraphDeviceBitlockerRecoveryKeyDetails = "informationProtection/bitlocker/recoveryKeys/{0}?$select=key";
        public const string constMsGraphDeviceLAPS = "directory/deviceLocalCredentials/{0}?$select=credentials,deviceName";
        public const string constMsGraphTAP = "users/{0}/authentication/temporaryAccessPassMethods";
        public const int constMsGraphManagedDevicePaging = 100;
        public const string constTableNameIntuneDeviceDetails = "intune-deviceInfo";
        public const string constTableNameIntuneUserDeviceCount = "intune-userDeviceCount";
        public const string constTableNameIntuneUserDeviceCountDetail = "intune-userDeviceCount-details";
        public const string constTableNameIntuneMobileDeviceDetails = "intune-mobile-deviceInfo";
        public readonly List<cLdapPropertyInfo> UserMainProperties = new List<cLdapPropertyInfo>() {
            new cLdapPropertyInfo() { Name="objectSid", LdapType= DataHistoryProvider.enumLdapPropertyType.ldapSid, DbName=null},
            new cLdapPropertyInfo() { Name="cn", LdapType= DataHistoryProvider.enumLdapPropertyType.ldapString, DbName=null},
            new cLdapPropertyInfo() { Name="DistinguishedName", LdapType= DataHistoryProvider.enumLdapPropertyType.ldapString, DbName=null},
            new cLdapPropertyInfo() { Name="uSNChanged", LdapType= DataHistoryProvider.enumLdapPropertyType.ldapTimeInterval, DbName=null},
            new cLdapPropertyInfo() { Name="sAMAccountName", LdapType= DataHistoryProvider.enumLdapPropertyType.ldapString, DbName=null},
            new cLdapPropertyInfo() { Name="displayName", LdapType= DataHistoryProvider.enumLdapPropertyType.ldapString, DbName=null},
            new cLdapPropertyInfo() { Name="userPrincipalName", LdapType= DataHistoryProvider.enumLdapPropertyType.ldapString, DbName="upn_external"},
            new cLdapPropertyInfo() { Name="userAccountControl", LdapType= DataHistoryProvider.enumLdapPropertyType.ldapString, DbName=null},
            new cLdapPropertyInfo() { Name="mail", LdapType= DataHistoryProvider.enumLdapPropertyType.ldapString, DbName="mail"}
        };
        public readonly List<cLdapPropertyInfo> UserPhoneProperties = new List<cLdapPropertyInfo>() {
            new cLdapPropertyInfo() { Name="telephoneNumber", LdapType= DataHistoryProvider.enumLdapPropertyType.ldapPhone, DbName="phone", Cumulative = true, Reverse = true},
            new cLdapPropertyInfo() { Name="mobile", LdapType= DataHistoryProvider.enumLdapPropertyType.ldapPhone, DbName="phone", Cumulative = true, Reverse = true},
            new cLdapPropertyInfo() { Name="ipPhone", LdapType= DataHistoryProvider.enumLdapPropertyType.ldapPhone, DbName="phone", Cumulative = true, Reverse = true},
            new cLdapPropertyInfo() { Name="otherTelephone", LdapType= DataHistoryProvider.enumLdapPropertyType.ldapPhone, DbName="phone", Cumulative = true, Reverse = true},
            new cLdapPropertyInfo() { Name="otherMobile", LdapType= DataHistoryProvider.enumLdapPropertyType.ldapPhone, DbName="phone", Cumulative = true, Reverse = true},
            new cLdapPropertyInfo() { Name="otherIpPhone", LdapType= DataHistoryProvider.enumLdapPropertyType.ldapPhone, DbName="phone", Cumulative = true, Reverse = true}
        };

        public readonly List<cLdapPropertyInfo> ComputerMainProperties = new List<cLdapPropertyInfo>() {
            new cLdapPropertyInfo() { Name="objectSid", LdapType= DataHistoryProvider.enumLdapPropertyType.ldapSid, DbName=null},
            new cLdapPropertyInfo() { Name="cn", LdapType= DataHistoryProvider.enumLdapPropertyType.ldapString, DbName=null},
            new cLdapPropertyInfo() { Name="DistinguishedName", LdapType= DataHistoryProvider.enumLdapPropertyType.ldapString, DbName=null},
            new cLdapPropertyInfo() { Name="uSNChanged", LdapType= DataHistoryProvider.enumLdapPropertyType.ldapTimeInterval, DbName=null},
            new cLdapPropertyInfo() { Name="sAMAccountName", LdapType= DataHistoryProvider.enumLdapPropertyType.ldapString, DbName=null},
            new cLdapPropertyInfo() { Name="userAccountControl", LdapType= DataHistoryProvider.enumLdapPropertyType.ldapString, DbName=null}
        };
        private readonly cDataHistoryCollector _collector;
        private static readonly DateTime BaseDateTime;
        private static readonly long MinSqlDateTimeTicks = 0;
        private static readonly long MaxSqlDateTimeTicks = 0;

        private readonly Dictionary<string, string> Domains = new Dictionary<string, string>();
        private readonly Dictionary<string, string> DomainsRev = new Dictionary<string, string>();
        private DateTime LastDomainsUpdate = DateTime.MinValue;
        private bool DomainsUpdateRunning = false;
        private Dictionary<Guid, cMsGraphBase> tenantGraphCache = new Dictionary<Guid, cMsGraphBase>();
        public bool WinAuthenticationGropsResolved { get; private set; }
        static cDataHistoryCollectorActiveDirectory()
        {
            BaseDateTime = new DateTime(1601, 1, 1, 0, 0, 0, DateTimeKind.Utc);
            MinSqlDateTimeTicks = (long)(new DateTime(1753, 1, 1, 0, 0, 0, DateTimeKind.Utc) - BaseDateTime).TotalMilliseconds * 10000 + 1;
            var dt = DateTime.SpecifyKind(DateTime.MaxValue, DateTimeKind.Utc) - TimeSpan.FromDays(1);
            MaxSqlDateTimeTicks = (long)(dt - BaseDateTime).TotalMilliseconds * 10000 - 1;
        }

        public cDataHistoryCollectorActiveDirectory(cDataHistoryCollector Collector)
            : base(Collector, new List<enumDataHistoryOrigin>() { enumDataHistoryOrigin.ActiveDirectory, enumDataHistoryOrigin.Intune }, constConnectorName, constLicenseId)
        {
            _collector = Collector;
        }
        public HashSet<enumFasdInformationClass> GetSupportedInformationClasses() => new HashSet<enumFasdInformationClass>() { enumFasdInformationClass.MobileDevice };
        private async Task<Dictionary<string, cDbAdScanInfo>> GetLastAdScansAsync(cDbConnection Conn, 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;

            if (!DataHistorySqlHelper.GetWellKnownSqlStatement("GetAdScanInfo", out var Query))
                return null;

            var Retval = new Dictionary<string, cDbAdScanInfo>();

            int apiError = 0;

            try
            {


                if (Token.IsCancellationRequested)
                    return null;

                DbDataReader Reader = await DataHistorySqlHelper.GetTableResultAsync(Conn, Query, null, Token);

                if (Token.IsCancellationRequested)
                    return null;

                if (Reader == null)
                    return null;

                try
                {
                    if (Reader.HasRows)
                    {
                        while (await Reader.ReadAsync())
                        {
                            if (Token.IsCancellationRequested)
                                return null;

                            try
                            {
                                var Entry = new cDbAdScanInfo
                                {
                                    ID = Reader.GetInt32(0),
                                    Name = Reader.GetString(1),
                                    LastRun = DataHistorySqlHelper.ConvertDBDateTimeToUTC(Reader.GetDateTime(2))
                                };
                                if (Retval.ContainsKey(Entry.Name))
                                    Retval[Entry.Name] = Entry;
                                else
                                    Retval.Add(Entry.Name, Entry);
                            }
                            catch (Exception E)
                            {
                                apiError = E.HResult;
                                cLogManager.DefaultLogger.LogException(E);
                            }
                        }
                    }
                }
                catch (Exception E)
                {
                    apiError = E.HResult;
                    cLogManager.DefaultLogger.LogException(E);
                }
                finally
                {
                    Reader.Close();
                    Reader.Dispose();
                }


                return Retval;
            }
            catch (Exception E)
            {
                apiError = E.HResult;
                cLogManager.DefaultLogger.LogException(E);
            }
            finally
            {
                if (DataHistorySqlHelper.LogSql) DataHistorySqlHelper.SaveSqlTimingEntry(Query.Name, _startTime, "", apiError, requestInfo?.requestName);
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }

            return null;
        }

        private async Task RemoveAdScanEntryAsync(cDbConnection Conn, int ID, 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 (!DataHistorySqlHelper.GetWellKnownSqlStatement("RemoveAdScanInfo", out var Query))
                    return;

                if (Token.IsCancellationRequested)
                    return;

                var Params = new Dictionary<string, object>
                    {
                        { "ID", ID }
                    };
                var Cnt = await DataHistorySqlHelper.ExecuteAsync(Conn, Query, Params, requestInfo, Token);
            }
            catch (Exception E)
            {
                cLogManager.DefaultLogger.LogException(E);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }

            return;
        }

        private async Task<int> UpdateAdScanEntryAsync(cDbConnection Conn, cDbAdScanInfo Entry, 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 (!DataHistorySqlHelper.GetWellKnownSqlStatement("UpdateAdScanInfo", out var Query))
                    return -1;


                if (Token.IsCancellationRequested)
                    return -1;

                var Params = new Dictionary<string, object>
                    {
                        { "ID", Entry.ID },
                        { "Name", Entry.Name },
                        { "LastRun", Entry.LastRun }
                    };
                var ID = await DataHistorySqlHelper.GetScalarResultAsync<int>(Conn, Query, Params, requestInfo, Token);

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

            return -1;
        }

        private async Task<Dictionary<string, cDbAdScanNode>> GetLastAdScanNode(int ScanID, 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;


            if (!DataHistorySqlHelper.GetWellKnownSqlStatement("GetAdScanNodes", out var Query))
                return null;

            var Params = new Dictionary<string, object>
                    {
                        { "ScanID", ScanID }
                    };

            int apiError = 0;

            var Retval = new Dictionary<string, cDbAdScanNode>();
            try
            {

                using (var Conn = new cDbConnection(Collector.mainDbConnection))
                {

                    if (Token.IsCancellationRequested)
                        return null;

                    DbDataReader Reader = await DataHistorySqlHelper.GetTableResultAsync(Conn, Query, Params, Token);

                    if (Token.IsCancellationRequested)
                        return null;

                    if (Reader == null)
                        return null;

                    try
                    {
                        if (Reader.HasRows)
                        {
                            while (await Reader.ReadAsync())
                            {
                                if (Token.IsCancellationRequested)
                                    return null;

                                try
                                {
                                    var Entry = new cDbAdScanNode
                                    {
                                        ScanID = Reader.GetInt32(0),
                                        Name = Reader.GetString(1),
                                        Fingerprint = Reader.GetString(2),
                                        InvocationID = Reader.GetString(3)
                                    };
                                    var o = Reader[4];
                                    if ((o == null) || (o is DBNull))
                                        Entry.LastUSN = 0;
                                    else
                                    {
                                        Decimal d = (Decimal)o;
                                        Entry.LastUSN = Convert.ToUInt64(d);
                                    }
                                    if (Retval.ContainsKey(Entry.Name) && (Entry.ScanID == ScanID))
                                        Retval[Entry.Name] = Entry;
                                    else
                                        Retval.Add(Entry.Name, Entry);
                                }
                                catch (Exception E)
                                {
                                    cLogManager.DefaultLogger.LogException(E);
                                }
                            }
                        }
                    }
                    catch (Exception E)
                    {
                        cLogManager.DefaultLogger.LogException(E);
                    }
                    finally
                    {
                        Reader.Close();
                        Reader.Dispose();
                    }

                }

                return Retval;
            }
            catch (Exception E)
            {
                apiError = E.HResult;
                cLogManager.DefaultLogger.LogException(E);
            }
            finally
            {
                if (DataHistorySqlHelper.LogSql) DataHistorySqlHelper.SaveSqlTimingEntry(Query.Name, _startTime, Params, apiError, requestInfo?.requestName);
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }

            return null;
        }

        private async Task UpdateAdScanNodeEntryAsync(cDbAdScanNode Node, 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 (!DataHistorySqlHelper.GetWellKnownSqlStatement("UpdateAdScanNode", out var Query))
                    return;

                using (var Conn = new cDbConnection(Collector.mainDbConnection))
                {

                    if (Token.IsCancellationRequested)
                        return;

                    var Params = new Dictionary<string, object>
                    {
                        { "ScanID", Node.ScanID },
                        { "Name", Node.Name },
                        { "Fingerprint", Node.Fingerprint },
                        { "InvocationId", Node.InvocationID },
                        { "LastUsn", Convert.ToDecimal(Node.LastUSN) }
                    };

                    await DataHistorySqlHelper.ExecuteAsync(Conn, Query, Params, requestInfo, Token);
                }
            }
            catch (Exception E)
            {
                cLogManager.DefaultLogger.LogException(E);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }
        }

        private static string GetStringFromLdapAttribute(SearchResultAttributeCollection Attributes, string Name, string ObjectName, int MaxLength = -1, Dictionary<string, cLdapPropertyInfo> Props = null)
        {
            try
            {
                if (Attributes.Contains(Name))
                {
                    var arrAttr = Attributes[Name];
                    if ((arrAttr != null) && (arrAttr.Count > 0))
                    {
                        var objAttr = arrAttr[0];
                        if (objAttr == null)
                            return null;

                        var strAttr = objAttr.ToString();
                        if (MaxLength > 0 && strAttr.Length > MaxLength)
                            strAttr = strAttr.Substring(0, MaxLength);
                        if (Props != null && Props.TryGetValue(Name, out var _prop))
                        {
                            if (_prop.Cardinal > 0 && strAttr.Length > _prop.Cardinal)
                                strAttr = strAttr.Substring(0, _prop.Cardinal);
                        }
                        return strAttr;
                    }
                }

            }
            catch (Exception E)
            {
                cLogManager.DefaultLogger.LogEntry(LogLevels.Debug, string.Format("error getting LDAP property '{0}' for object '{1}'", Name, ObjectName));
                cLogManager.DefaultLogger.LogException(E, LogLevels.Debug);
            }

            return null;
        }

        private static Int32? GetInt32FromLdapAttribute(SearchResultAttributeCollection Attributes, string Name, string ObjectName)
        {
            try
            {
                if (Attributes.Contains(Name))
                {
                    var arrAttr = Attributes[Name];
                    if ((arrAttr != null) && (arrAttr.Count > 0))
                    {
                        var objAttr = arrAttr[0];
                        if (objAttr == null)
                            return null;

                        var strAttr = objAttr.ToString();
                        if (Int32.TryParse(strAttr, out var RetVal))
                            return RetVal;
                    }
                }

            }
            catch (Exception E)
            {
                cLogManager.DefaultLogger.LogEntry(LogLevels.Debug, string.Format("error getting LDAP property '{0}' for object '{1}'", Name, ObjectName));
                cLogManager.DefaultLogger.LogException(E, LogLevels.Debug);
            }

            return null;
        }

        private static UInt32? GetUInt32FromLdapAttribute(SearchResultAttributeCollection Attributes, string Name, string ObjectName)
        {
            try
            {
                if (Attributes.Contains(Name))
                {
                    var arrAttr = Attributes[Name];
                    if ((arrAttr != null) && (arrAttr.Count > 0))
                    {
                        var objAttr = arrAttr[0];
                        if (objAttr == null)
                            return null;

                        var strAttr = objAttr.ToString();
                        if (UInt32.TryParse(strAttr, out var RetVal))
                            return RetVal;
                    }
                }

            }
            catch (Exception E)
            {
                cLogManager.DefaultLogger.LogEntry(LogLevels.Debug, string.Format("error getting LDAP property '{0}' for object '{1}'", Name, ObjectName));
                cLogManager.DefaultLogger.LogException(E, LogLevels.Debug);
            }

            return null;
        }

        private static UInt64? GetUInt64FromLdapAttribute(SearchResultAttributeCollection Attributes, string Name, string ObjectName)
        {
            try
            {
                if (Attributes.Contains(Name))
                {
                    var arrAttr = Attributes[Name];
                    if ((arrAttr != null) && (arrAttr.Count > 0))
                    {
                        var objAttr = arrAttr[0];
                        if (objAttr == null)
                            return null;

                        var strAttr = objAttr.ToString();
                        if (UInt64.TryParse(strAttr, out var RetVal))
                            return RetVal;
                    }
                }

            }
            catch (Exception E)
            {
                cLogManager.DefaultLogger.LogEntry(LogLevels.Debug, string.Format("error getting LDAP property '{0}' for object '{1}'", Name, ObjectName));
                cLogManager.DefaultLogger.LogException(E, LogLevels.Debug);
            }

            return null;
        }

        private static DateTime? GetDateTimeFromLdapAttribute(SearchResultAttributeCollection Attributes, string Name, string ObjectName)
        {
            try
            {
                if (Attributes.Contains(Name))
                {
                    var arrAttr = Attributes[Name];
                    if ((arrAttr != null) && (arrAttr.Count > 0))
                    {
                        var objAttr = arrAttr[0];
                        if (objAttr == null)
                            return null;

                        var strAttr = objAttr.ToString();
                        if (strAttr.EndsWith("Z"))
                        {
                            strAttr = strAttr.Remove(strAttr.Length - 1, 1);
                            if (DateTime.TryParseExact(strAttr, "yyyyMMddHHmmss.f", CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.AdjustToUniversal, out var dt))
                            {
                                dt = DateTime.SpecifyKind(dt, DateTimeKind.Utc);
                                return dt;
                            }
                        }
                        else
                        {
                            if (long.TryParse(strAttr, out var ticks))
                            {
                                if (ticks <= MinSqlDateTimeTicks || ticks >= MaxSqlDateTimeTicks)
                                    return null;

                                long msecs = ticks / 10000;
                                var ts = TimeSpan.FromMilliseconds(msecs);
                                var RetVal = BaseDateTime + ts;
                                return RetVal;
                            }
                        }
                    }
                }

            }
            catch (Exception E)
            {
                cLogManager.DefaultLogger.LogEntry(LogLevels.Debug, string.Format("error getting LDAP property '{0}' for object '{1}'", Name, ObjectName));
                cLogManager.DefaultLogger.LogException(E, LogLevels.Debug);
            }

            return null;
        }

        private static object GetValueFromLdapAttribute(SearchResultAttributeCollection Attributes, string Name, string ObjectName)
        {
            try
            {
                if (Attributes.Contains(Name))
                {
                    var arrAttr = Attributes[Name];
                    if ((arrAttr != null) && (arrAttr.Count > 0))
                    {
                        var objAttr = arrAttr[0];
                        if (objAttr != null)
                            return objAttr;
                    }
                }

            }
            catch (Exception E)
            {
                cLogManager.DefaultLogger.LogEntry(LogLevels.Debug, string.Format("error getting LDAP property '{0}' for object '{1}'", Name, ObjectName));
                cLogManager.DefaultLogger.LogException(E, LogLevels.Debug);
            }

            return null;
        }

        private static byte[] GetByteArrayFromLdapAttribute(SearchResultAttributeCollection Attributes, string Name, string ObjectName)
        {
            try
            {
                if (Attributes.Contains(Name))
                {
                    var arrAttr = Attributes[Name];
                    if ((arrAttr != null) && (arrAttr.Count > 0))
                    {
                        var objAttr = arrAttr[0];
                        if (objAttr is byte[] RetVal)
                            return RetVal;
                    }
                }

            }
            catch (Exception E)
            {
                cLogManager.DefaultLogger.LogEntry(LogLevels.Debug, string.Format("error getting LDAP property '{0}' for object '{1}'", Name, ObjectName));
                cLogManager.DefaultLogger.LogException(E, LogLevels.Debug);
            }

            return null;
        }

        private static string GetSidFromByteArray(byte[] B, string Name, string ObjectName)
        {
            try
            {
                var objSid = new SecurityIdentifier(B, 0);
                var strSid = objSid.ToString();
                return strSid;

            }
            catch (Exception E)
            {
                cLogManager.DefaultLogger.LogEntry(LogLevels.Debug, string.Format("error converting ldap property '{0}' for object '{1}' to SID", Name, ObjectName));
                cLogManager.DefaultLogger.LogException(E, LogLevels.Debug);
            }

            return null;
        }

        public static byte[] GetByteArrayFromSid(string Sid)
        {
            try
            {
                var objSid = new SecurityIdentifier(Sid);
                byte[] RetVal = new byte[objSid.BinaryLength];
                objSid.GetBinaryForm(RetVal, 0);
                return RetVal;
            }
            catch (Exception E)
            {
                cLogManager.DefaultLogger.LogEntry(LogLevels.Warning, string.Format($"error converting string to SID: {Sid}"));
                cLogManager.DefaultLogger.LogException(E, LogLevels.Debug);
            }

            return null;
        }

        private static string FormatPlainPhone(string s)
        {
            if (s == null)
                return null;

            var RetVal = "";
            foreach (var C in s)
                if ((C >= '0') && (C <= '9'))
                    RetVal += C;

            return RetVal;
        }

        private void GetSubOUs(LdapConnection LC, List<string> nodeList, string DSN, List<string> AlreadyScannedNodes)
        {
            var UL = new List<string>();

            try
            {
                foreach (var AlreadyNode in AlreadyScannedNodes)
                {
                    if (AlreadyNode == DSN.ToUpperInvariant())
                        return;
                }

                PageResultRequestControl pageRequestControl = new PageResultRequestControl((int)1000);
                PageResultResponseControl pageResponseControl;
                SearchOptionsControl soc = new SearchOptionsControl(System.DirectoryServices.Protocols.SearchOption.DomainScope);

                string[] Attr = { "ou", "DistinguishedName" };
                var SR = new SearchRequest(DSN, "(objectclass=organizationalUnit)", System.DirectoryServices.Protocols.SearchScope.OneLevel, Attr);
                SR.Controls.Add(pageRequestControl);
                SR.Controls.Add(soc);

                while (true)
                {
                    var searchResponse = (SearchResponse)LC.SendRequest(SR);
                    pageResponseControl = (PageResultResponseControl)searchResponse.Controls[0];
                    foreach (SearchResultEntry Entry in searchResponse.Entries)
                    {
                        try
                        {
                            var dn = Entry.Attributes["DistinguishedName"][0].ToString();
                            UL.Add(dn);
                        }
                        catch { }
                    }

                    if (pageResponseControl.Cookie.Length == 0)
                        break;

                    pageRequestControl.Cookie = pageResponseControl.Cookie;
                }

                foreach (var DN in UL)
                {
                    var skip = false;
                    foreach (var AlreadyNode in AlreadyScannedNodes)
                    {
                        if (AlreadyNode == DN.ToUpperInvariant())
                        {
                            skip = true;
                            break;
                        }
                    }
                    if (!skip)
                    {
                        nodeList.Add(DN);
                        GetSubOUs(LC, nodeList, DN, AlreadyScannedNodes);
                    }
                }
            }
            catch (Exception E)
            {
                cLogManager.DefaultLogger.LogEntry(LogLevels.Error, string.Format("Error getting sub OUs from '{0}' at server {1}", DSN, LC.SessionOptions.HostName));
                cLogManager.DefaultLogger.LogException(E);
                return;
            }

        }

        private async Task<string> DoAdScanAsync(cDbConnection Conn, cDataHistoryConfigActiveDirectory AdConfig, Dictionary<string, cLdapPropertyInfo> LdapProps, cDataHistoryAdScan Scan, cDataHistoryAdScanNode Node, List<string> AlreadyScannedNodes, cDbAdScanNode NodeInfo, cDbAdGeneralInfo GeneralInfo, bool Rescan, 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
            {
                cDataHistoryAdServer Server = null;
                string InvocationId = null;
                UInt64 maxUsn = 0;
                LdapConnection LC = null;

                var AdDomain = Node.AdDomain;
                var Creds = new NetworkCredential(AdDomain.Credential.User, AdDomain.Credential.NwCredential.Password, AdDomain.Credential.Domain);

                foreach (var tmpServer in AdDomain.Servers)
                {
                    maxUsn = 0;

                    // create the Ldap connection object
                    var LdapInfo = new LdapDirectoryIdentifier(tmpServer.FQDN, tmpServer.Port, false, false);
                    LC = new LdapConnection(LdapInfo);
                    LC.SessionOptions.ProtocolVersion = 3;
                    LC.Credential = Creds;

                    // try get the server invocationId
                    try
                    {
                        string[] Attr0 = { "dsServiceName", "highestCommittedUSN", "defaultNamingContext", "configurationNamingContext" };
                        var SR1 = new SearchRequest(null, "(objectclass=*)", System.DirectoryServices.Protocols.SearchScope.Base, Attr0);
                        var SRR = (SearchResponse)LC.SendRequest(SR1);
                        var currentDomain = SRR.Entries[0].Attributes["defaultNamingContext"][0].ToString();
                        var CNC = SRR.Entries[0].Attributes["configurationNamingContext"][0].ToString();
                        NodeInfo.domainDNS = "";
                        var arrDoms = currentDomain.Split(',');
                        if (arrDoms != null)
                            for (int i = 0; i < arrDoms.Length; i++)
                            {
                                var arr2 = arrDoms[i].Split('=');
                                if (arr2 == null || arr2.Length != 2 || string.IsNullOrEmpty(arr2[1]))
                                    break;
                                if (NodeInfo.domainDNS != "")
                                    NodeInfo.domainDNS += ".";
                                NodeInfo.domainDNS += arr2[1].ToLowerInvariant();
                            }

                        if (NodeInfo.domainDNS != AdDomain.FQDN.ToLowerInvariant())
                        {
                            LogEntry($"The DC server '{tmpServer.FQDN}' is not member of the domain '{AdDomain.FQDN}'.", LogLevels.Warning);
                            Collector.DoProcessUiMessage(2, $"Warning: The DC server '{tmpServer.FQDN}' is not member of the domain '{AdDomain.FQDN}', it is member of '{NodeInfo.domainDNS}'.");
                            continue;
                        }

                        var SN = SRR.Entries[0].Attributes["dsServiceName"][0].ToString();
                        var strMaxUsn = SRR.Entries[0].Attributes["highestcommittedusn"][0].ToString();
                        if (!UInt64.TryParse(strMaxUsn, out maxUsn))
                            maxUsn = 0;

                        string[] Attr1 = { "invocationId" };
                        SR1 = new SearchRequest(SN, "(objectclass=*)", System.DirectoryServices.Protocols.SearchScope.Base, Attr1);
                        SRR = (SearchResponse)LC.SendRequest(SR1);
                        var arrId = (byte[])SRR.Entries[0].Attributes["invocationId"][0];
                        InvocationId = "";
                        foreach (var b in arrId)
                            InvocationId += b.ToString("X2");

                        NodeInfo.domainNB = null;
                        try
                        {
                            string[] Attr2 = { "netbiosname" };
                            var filter = $"(&(objectclass=crossRef)(nCName={currentDomain}))";
                            //var filter = $"(objectclass=crossRef)";
                            SR1 = new SearchRequest("CN=Partitions," + CNC, filter, System.DirectoryServices.Protocols.SearchScope.OneLevel, Attr2);
                            SRR = (SearchResponse)LC.SendRequest(SR1);
                            NodeInfo.domainNB = SRR.Entries[0].Attributes["netbiosname"][0].ToString().ToUpperInvariant();
                        }
                        catch (Exception E)
                        {
                            LogEntry("", LogLevels.Warning);
                            LogException(E, LogLevels.Warning);
                        }

                        if (!string.IsNullOrEmpty(NodeInfo.domainNB))
                        {
                            if (!GeneralInfo.Domains.ContainsKey(NodeInfo.domainDNS))
                                GeneralInfo.Domains[NodeInfo.domainDNS] = NodeInfo.domainNB;
                        }
                    }
                    catch (Exception E)
                    {
                        cLogManager.DefaultLogger.LogEntry(LogLevels.Debug, string.Format("Could not get AD invocation id from server {0}", tmpServer));
                        cLogManager.DefaultLogger.LogException(E, LogLevels.Debug);
                        InvocationId = null;
                    }

                    if (!string.IsNullOrEmpty(InvocationId))
                    {
                        Server = tmpServer;
                        break;
                    }
                }


                // exit, if no login was sucessfull
                if (Server == null)
                {
                    Collector.DoProcessUiMessage(2, $"Error: Could not get a valid AD server with invocation id for domain '{AdDomain.FQDN}', aborting scan...");
                    LogEntry("Could not get a valid AD server with invocation id, aborting AD scan.", LogLevels.Error);
                    return null;
                }

                // check, if we have to do a full scan
                var Fingerprint = string.Format("{0}|{1}|{2}|{3}", Scan.Name, Node.AdDomain, Node.Path, Server.FQDN);
                Fingerprint = GetHash(Fingerprint);
                bool bFullScan = true;
                if ((NodeInfo.Fingerprint == Fingerprint) && (NodeInfo.InvocationID == InvocationId) && (NodeInfo.LastUSN > 0) && !Rescan)
                    bFullScan = false;
                else
                {
                    NodeInfo.Fingerprint = Fingerprint;
                    NodeInfo.InvocationID = InvocationId;
                    NodeInfo.LastUSN = 0;
                }

                // create the ldap filter & the list of ldap poperties needed
                string ldapFilter;
                string[] defAttr;
                if (Scan.Type == enumFasdInformationClass.User)
                {
                    ldapFilter = "(objectClass=user)(objectCategory=person)";
                }
                else if (Scan.Type == enumFasdInformationClass.Computer)
                {
                    ldapFilter = "(objectClass=computer)(objectCategory=computer)";
                }
                else
                {
                    LogEntry($"Illegal scan tpye '{Scan.Type}' used, aborting AD scan.", LogLevels.Error);
                    return null;
                }
                defAttr = LdapProps.Keys.ToArray();

                if (!bFullScan)
                    ldapFilter += string.Format("(!(uSNChanged<={0}))", NodeInfo.LastUSN.ToString());

                if (!string.IsNullOrEmpty(Node.LdapFilter))
                {
                    var strFilter = Node.LdapFilter;
                    if (!(strFilter.StartsWith("(") && strFilter.EndsWith(")")))
                        strFilter = "(" + strFilter + ")";
                    ldapFilter += strFilter;
                }

                ldapFilter = "(&" + ldapFilter + ")";

                // get the list of sub ous
                var nodeList = new List<string>();
                var DN = GetBaseDN(Node);
                nodeList.Add(DN);
                GetSubOUs(LC, nodeList, DN, AlreadyScannedNodes);
                AlreadyScannedNodes.Add(DN.ToUpperInvariant());

                Collector.DoProcessUiMessage(2, string.Format("Scanning {0} OUs on server {1}...", nodeList.Count, Server.FQDN));

                // scan the ous
                int numChanges = 0;
                foreach (var dnOU in nodeList)
                {
                    try
                    {
                        Collector.DoProcessUiMessage(4, $"scan node '{dnOU}'");

                        PageResultRequestControl pageRequestControl = new PageResultRequestControl((int)1000);
                        PageResultResponseControl pageResponseControl;
                        SearchOptionsControl soc = new SearchOptionsControl(System.DirectoryServices.Protocols.SearchOption.DomainScope);

                        var SR = new SearchRequest(dnOU, ldapFilter, System.DirectoryServices.Protocols.SearchScope.OneLevel, defAttr);
                        var _al = defAttr.ToList();
                        _al.Insert(0, "Search request with attribute list:");
                        cLogManager.DefaultLogger.LogList(LogLevels.Debug, _al);
                        SR.Controls.Add(pageRequestControl);
                        SR.Controls.Add(soc);

                        while (true)
                        {
                            var searchResponse = (SearchResponse)LC.SendRequest(SR);
                            pageResponseControl = (PageResultResponseControl)searchResponse.Controls[0];

                            if (searchResponse?.Entries?.Count > 0)
                            {
                                LogEntry($" processing {searchResponse.Entries.Count} entries for scan node '{dnOU}'", LogLevels.Debug);
                                foreach (SearchResultEntry Entry in searchResponse.Entries)
                                {
                                    try
                                    {
                                        bool res = await ProcessSearchEntry(Conn, Entry, LdapProps, Scan, Node, NodeInfo, requestInfo, LogDeep + 1, Token);
                                        if (res)
                                            numChanges++;
                                    }
                                    catch (Exception E)
                                    {
                                        cLogManager.DefaultLogger.LogException(E);
                                    }
                                }
                            }

                            if (pageResponseControl.Cookie.Length == 0)
                                break;

                            pageRequestControl.Cookie = pageResponseControl.Cookie;
                        }

                        LogEntry($" finished scan node '{dnOU}'", LogLevels.Debug);
                    }
                    catch (Exception E)
                    {
                        cLogManager.DefaultLogger.LogException(E);
                    }

                }

                Collector.DoProcessUiMessage(2, string.Format("... finished, {0} updates processed", numChanges));

                NodeInfo.LastUSN = maxUsn;

                return Server.FQDN;
            }
            catch (Exception E)
            {
                cLogManager.DefaultLogger.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> ProcessSearchEntry(cDbConnection Conn, SearchResultEntry Entry, Dictionary<string, cLdapPropertyInfo> LdapProps, cDataHistoryAdScan Scan, cDataHistoryAdScanNode Node, cDbAdScanNode NodeInfo, 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 SqlParams = new Dictionary<string, object>();

                // get the USN
                var cName = GetStringFromLdapAttribute(Entry.Attributes, "cn", Entry.DistinguishedName, Props: LdapProps);
                var account = GetStringFromLdapAttribute(Entry.Attributes, "sAMAccountName", Entry.DistinguishedName, Props: LdapProps);

                if (Node.WildcardFilter != null || Node.RegExFilter != null)
                {
                    string strFilter;
                    switch (Node.FilterProperty)
                    {
                        case cDataHistoryAdScanNode.enumFilterPropery.distinguishedName:
                            strFilter = Entry.DistinguishedName;
                            break;
                        case cDataHistoryAdScanNode.enumFilterPropery.samAccountName:
                            strFilter = account;
                            break;
                        default:
                            strFilter = cName;
                            break;
                    }

                    if (Node.WildcardFilter != null)
                        if (Node.WildcardFilter.IsMatch(strFilter) ^ !Node.WildcardFilterNot)
                            return false;
                    if (Node.RegExFilter != null)
                        if (!Node.RegExFilter.IsMatch(strFilter))
                            return false;
                }

                var strSNC = GetStringFromLdapAttribute(Entry.Attributes, "usnchanged", Entry.DistinguishedName, Props: LdapProps);
                if (!UInt64.TryParse(strSNC, out ulong uSNChanged))
                    uSNChanged = 0;

                if (cLogManager.DefaultLogger.IsDebug)
                {
                    var _al = new List<string> { $"SearchResultEntry attributes for '{Entry.DistinguishedName}':" };
                    foreach (var _E in Entry.Attributes.AttributeNames)
                        _al.Add(_E?.ToString());
                    cLogManager.DefaultLogger.LogList(LogLevels.Debug, _al);
                }

                if (uSNChanged >= NodeInfo.LastUSN)
                {
                    // get the SID
                    var Sid = GetByteArrayFromLdapAttribute(Entry.Attributes, "objectSid", Entry.DistinguishedName);
                    if (Sid == null)
                        return false;

                    var strSid = GetSidFromByteArray(Sid, "objectSid", Entry.DistinguishedName);
                    if (string.IsNullOrEmpty(strSid))
                        return false;

                    SqlParams.Add("sid", strSid);
                    SqlParams.Add("sid_bin", Sid);

                    // get the account name & internal UPN
                    if (string.IsNullOrEmpty(account))
                        return false;

                    var internalUPN = account.ToLowerInvariant() + "@" + Node.AdDomain.FQDN;

                    if (Scan.Type == enumFasdInformationClass.User)
                        SqlParams.Add("account", account);

                    SqlParams.Add("upn_internal", internalUPN);

                    // get the userAccountControl & active flag
                    int? isActive = null;
                    var Uac = GetUInt32FromLdapAttribute(Entry.Attributes, "userAccountControl", Entry.DistinguishedName);
                    if (Uac is UInt32 Uac2)
                    {
                        if ((Uac2 & 2) == 0)
                            isActive = 1;
                        else
                            isActive = 0;
                    }
                    if (isActive == null)
                    {
                        if (Entry.Attributes.Contains("userAccountControl"))
                        {
                            var arrAttr = Entry.Attributes["userAccountControl"];
                            if ((arrAttr != null) && (arrAttr.Count > 0))
                            {
                                var objAttr = arrAttr[0];
                                if (objAttr == null)
                                {
                                    LogEntry($"The ldap result for '{Entry.DistinguishedName}' contains property 'userAccountControl' with value NULL", LogLevels.Debug);
                                }
                                else
                                {
                                    var strAttr = objAttr.ToString();
                                    LogEntry($"The ldap result for '{Entry.DistinguishedName}' contains property 'userAccountControl' with value string {strAttr}", LogLevels.Debug);
                                    if (!UInt32.TryParse(strAttr, out var RetVal))
                                        LogEntry($"The ldap result for '{Entry.DistinguishedName}' has a property which is not UINT32", LogLevels.Debug);
                                }
                            }
                            else
                                LogEntry($"The ldap result for '{Entry.DistinguishedName}' has an empty property 'userAccountControl'", LogLevels.Debug);

                        }
                        else
                        {
                            LogEntry($"The ldap result for '{Entry.DistinguishedName}' do not contain the property 'userAccountControl'", LogLevels.Debug);
                        }
                    }

                    SqlParams.Add("is_active", isActive);
                    if (Scan.Type == enumFasdInformationClass.User)
                    {
                        SqlParams.Add("domainDns", NodeInfo.domainDNS);
                        SqlParams.Add("domainNB", NodeInfo.domainNB);
                    }

                    GetSqlParameters(Entry, LdapProps, ref SqlParams);

                    bool res = false;
                    if (Scan.Type == enumFasdInformationClass.User)
                        res = await ProcessSearchEntryUser(Conn, Entry, SqlParams, LdapProps, requestInfo, LogDeep + 1, Token);
                    else if (Scan.Type == enumFasdInformationClass.Computer)
                        res = await ProcessSearchEntryComputer(Conn, SqlParams, requestInfo, LogDeep + 1, Token);

                    if (res)
                        await ProcessAdditionalTables(Conn, Entry, Scan.Type, Node, NodeInfo, requestInfo, LogDeep + 1, Token);

                    return res;
                }
            }
            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 void GetSqlParameters(SearchResultEntry Entry, Dictionary<string, cLdapPropertyInfo> LdapProps, ref Dictionary<string, object> SqlParams)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            try
            {
                foreach (var Prop in LdapProps.Values)
                {
                    if (string.IsNullOrEmpty(Prop.DbName))
                        continue;

                    var objValue = GetValueFromLdapAttribute(Entry.Attributes, Prop.Name, Entry.DistinguishedName);

                    if (Prop.Cumulative)
                    {
                        var strVal = GetStringFromLdapAttribute(Entry.Attributes, Prop.Name, Entry.DistinguishedName, Prop.Cardinal);
                        switch (Prop.LdapType)
                        {
                            case DataHistoryProvider.enumLdapPropertyType.ldapString:
                                break;
                            case DataHistoryProvider.enumLdapPropertyType.ldapPhone:
                                strVal = FormatPlainPhone(strVal);
                                break;
                            default:
                                continue;
                        }
                        if (!string.IsNullOrWhiteSpace(strVal))
                        {
                            if (SqlParams.ContainsKey(Prop.DbName))
                                SqlParams[Prop.DbName] += " " + strVal;
                            else
                                SqlParams.Add(Prop.DbName, strVal);
                            if (Prop.Reverse)
                            {
                                char[] arrRev = (strVal + 'x').ToArray();
                                Array.Reverse(arrRev);
                                var strRev = new string(arrRev);
                                var pName = Prop.DbName + "_rev";
                                if (SqlParams.ContainsKey(pName))
                                    SqlParams[pName] += " " + strRev;
                                else
                                    SqlParams.Add(pName, strRev);
                            }
                        }
                    }
                    else
                    {
                        switch (Prop.LdapType)
                        {
                            case DataHistoryProvider.enumLdapPropertyType.ldapString:
                                var strVal = GetStringFromLdapAttribute(Entry.Attributes, Prop.Name, Entry.DistinguishedName, Prop.Cardinal);
                                SqlParams[Prop.DbName] = strVal;
                                break;
                        }
                    }
                }

                foreach (var Prop in LdapProps.Values)
                {
                    if (Prop.DbName == null)
                        continue;
                    if (!SqlParams.ContainsKey(Prop.DbName))
                        SqlParams.Add(Prop.DbName, DBNull.Value);
                    if (Prop.Cumulative && Prop.Reverse)
                    {
                        var strName = Prop.DbName + "_rev";
                        if (!SqlParams.ContainsKey(strName))
                            SqlParams.Add(strName, DBNull.Value);
                    }
                }
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (CM != null) LogMethodEnd(CM);
            }
        }

        private async Task<bool> ProcessSearchEntryUser(cDbConnection Conn, SearchResultEntry Entry, Dictionary<string, object> SqlParams, Dictionary<string, cLdapPropertyInfo> LdapProps, 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
            {
                // get the full name
                var fullName = GetStringFromLdapAttribute(Entry.Attributes, "displayName", Entry.DistinguishedName, Props: LdapProps);
                if (string.IsNullOrEmpty(fullName))
                    fullName = SqlParams["account"] as string;
                SqlParams.Add("full_name", fullName);

                var lastSeen = DateTime.UtcNow;
                SqlParams.Add("last_seen", lastSeen);

                var res = await DataHistorySqlHelper.DoUpdateOrInsertAsync(Conn, "main-user", SqlParams, new List<string>() { "sid_bin" }, requestInfo, LogDeep + 1, Token);

                if (res)
                    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;
        }

        private async Task<bool> ProcessSearchEntryComputer(cDbConnection Conn, Dictionary<string, object> SqlParams, 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 (!DataHistorySqlHelper.GetWellKnownSqlStatement("UpdateMainComputerLdap", out var QueryInsert))
                    return false;

                var ret = await DataHistorySqlHelper.ExecuteAsync(Conn, QueryInsert, SqlParams, requestInfo, Token);

                return ret > 0;
            }
            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 ProcessAdditionalTables(cDbConnection Conn, SearchResultEntry Entry, enumFasdInformationClass InformationClass, cDataHistoryAdScanNode Node, cDbAdScanNode NodeInfo, 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
            {
                foreach (var DataCluster in Collector.ClusterConfig.Clusters.Values)
                {
                    if (DataCluster.Origin != enumDataHistoryOrigin.ActiveDirectory || DataCluster.InformationClass != InformationClass)
                        continue;

                    foreach (var Table in DataCluster.Tables.Values)
                    {
                        if (Table.Type != eDataHistoryTableType.Static)
                            continue;


                        var SqlParams = new Dictionary<string, object>();

                        foreach (var col in Table.Columns.Values)
                        {
                            if (!(col is cDataHistoryConfigColumn tableCol))
                                continue;

                            if (SqlParams.ContainsKey(col.Name))
                                continue;

                            try
                            {
                                object Value = null;
                                object Value_bin = null;
                                bool IsBin = false;

                                switch (tableCol.SourceType)
                                {
                                    case eDataHistoryQueryType.Static:
                                        switch (tableCol.SourceName)
                                        {
                                            case "scan":
                                                Value = Node.parentScan?.Name;
                                                break;
                                            case "domainDns":
                                                Value = NodeInfo.domainDNS;
                                                break;
                                            case "domainNB":
                                                Value = NodeInfo.domainNB;
                                                break;
                                        }
                                        break;
                                    case eDataHistoryQueryType.Query:
                                        switch (tableCol.ValueType)
                                        {
                                            case enumFasdValueType.SID:
                                                IsBin = true;
                                                var arrSid = GetByteArrayFromLdapAttribute(Entry.Attributes, tableCol.SourceName, Entry.DistinguishedName);
                                                Value_bin = arrSid;
                                                if (Value_bin != null)
                                                    Value = GetSidFromByteArray(arrSid, tableCol.SourceName, Entry.DistinguishedName);
                                                break;
                                            case enumFasdValueType.STRING:
                                                Value = GetStringFromLdapAttribute(Entry.Attributes, tableCol.SourceName, Entry.DistinguishedName, tableCol.Cardinal);
                                                break;
                                            case enumFasdValueType.INT:
                                                Value = GetInt32FromLdapAttribute(Entry.Attributes, tableCol.SourceName, Entry.DistinguishedName);
                                                break;
                                            case enumFasdValueType.BIGINT:
                                                Value = (Int64?)GetUInt64FromLdapAttribute(Entry.Attributes, tableCol.SourceName, Entry.DistinguishedName);
                                                break;
                                            case enumFasdValueType.DATETIME:
                                                Value = GetDateTimeFromLdapAttribute(Entry.Attributes, tableCol.SourceName, Entry.DistinguishedName);
                                                break;
                                        }
                                        break;

                                }

                                SqlParams.Add(tableCol.Name, Value);
                                if (IsBin)
                                    SqlParams.Add(tableCol.Name + "_bin", Value_bin);
                            }
                            catch (Exception E)
                            {
                                LogEntry($"error getting ldap property '{tableCol.SourceName}' from object {Entry.DistinguishedName}", LogLevels.Warning);
                                LogException(E, LogLevels.Warning);
                            }
                        }

                        var lstIndex = new List<string>();
                        foreach (var Key in Table.KeyColumns)
                        {
                            if (Key.SqlTypeBin != null)
                                lstIndex.Add(Key.Name + "_bin");
                            else
                                lstIndex.Add(Key.Name);
                        }

                        var res = await DataHistorySqlHelper.DoUpdateOrInsertAsync(Conn, Table.SourceName, SqlParams, lstIndex, 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);
            }
        }

        private async Task ProcessScanNode(cDbConnection Conn, Dictionary<string, cLdapPropertyInfo> LdapProps, cDataHistoryAdScan Scan, cDataHistoryAdScanNode Node, List<string> AlreadyScannedNodes, cDbAdScanNode NodeInfo, cDbAdGeneralInfo GeneralInfo, bool Rescan, 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
            {
                try
                {
                    var Server = await Task<string>.Run(async () => { return await DoAdScanAsync(Conn, Collector.InfrastructureConfig.ActiveDirectory, LdapProps, Scan, Node, AlreadyScannedNodes, NodeInfo, GeneralInfo, Rescan, requestInfo, LogDeep + 1, Token); });
                }
                catch (Exception E)
                {
                    cLogManager.DefaultLogger.LogException(E);
                }

                // save the new run info
                await UpdateAdScanNodeEntryAsync(NodeInfo, 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);
            }

        }

        public async Task<cMsGraphBase> LogonAzureTenantAsync(cDataHistoryAzureTenant Tenant, bool force = false)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            try
            {
                var MsGraph = new cMsGraphBase();
                if (Tenant == null)
                {
                    return null;
                }
                var LI = new cMsGraphLogonInfo()
                {
                    Tenant = Tenant?.TenantID.ToString(),
                    ClientID = Tenant.Credential?.User,
                    ClientSecret = Tenant.Credential?.NwCredential.Password
                };
                var RetVal = await MsGraph.LogonAsync(LI);
                if (RetVal)
                    return MsGraph;
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (CM != null) LogMethodEnd(CM);
            }

            return null;
        }
        public async Task<cMsGraphBase> GetGraphForTenantAsync(Guid tenant, bool force = false)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            try
            {
                if (tenant == null)
                    return null;

                cDataHistoryAzureTenant _AzureTenant = null;
                var tenants = Collector.InfrastructureConfig.AzureTenants?.Values;
                if (tenants is null)
                {
                    return null;
                }
                foreach (var Tenant in tenants)
                {
                    if (Tenant.TenantID == tenant)
                    {
                        _AzureTenant = Tenant;
                        break;
                    }
                }
                if (_AzureTenant != null)
                {
                    if (_AzureTenant != null && tenantGraphCache.TryGetValue(_AzureTenant.TenantID, out var cachedGraph))
                        return cachedGraph;

                    var graph = await LogonAzureTenantAsync(_AzureTenant, force: force);
                    if (graph != null)
                        tenantGraphCache[_AzureTenant.TenantID] = graph;

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

            return null;
        }

        public async Task<bool> DoAzureScanAsync(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 tenants = Collector.InfrastructureConfig.AzureTenants?.Values;
                if (tenants is null)
                {
                    Collector.DoProcessUiMessage(2, $" could not logon to tenant - Tenant is null ", LogLevels.Error);
                    return true;
                }

                if (!DataHistorySqlHelper.GetWellKnownSqlStatement("UpdateMainUserAzureAd", out var QueryInsert))
                    return false;

                using (var Conn = new cDbConnection(Collector.mainDbConnection))
                {
                    if (!Conn.IsOpen)
                    {
                        LogEntry($"Could not open main sql database '{Collector.mainDbConnection.Database}', aborting scan...'", LogLevels.Error);
                        return false;
                    }


                    Collector.DoProcessUiMessage(0, string.Format("doing the Azure cloud scans:"));
                    foreach (var Tenant in tenants)
                    {
                        try
                        {
                            Collector.DoProcessUiMessage(1, $"scanning tenat {Tenant.Domain} for users ...");
                            var MsGraph = await GetGraphForTenantAsync(Tenant.TenantID, true);
                            if (MsGraph == null)
                            {
                                Collector.DoProcessUiMessage(2, $"error: could not logon to tenat {Tenant.Domain}", LogLevels.Error);
                                continue;
                            }

                            var strUrl = string.Format(constMsGraphUserList, constMsGraphUserPaging);

                            if (!string.IsNullOrEmpty(Tenant.ScanFilterUser))
                                strUrl += " and (" + Tenant.ScanFilterUser + ")";

                            var Result = await MsGraph.RequestListAsync(strUrl, UseBeta: Tenant.UseBetaUserFilter, retryForbidden: false, loadPaged: true, ConsistencyLevelEventual: Tenant.ConsistencyLevelEventualUserFilter);

                            var Count = 0;
                            var Updated = 0;
                            while (Result != null && Result.Count > 0)
                            {
                                foreach (var Entry in Result)
                                {
                                    if (Entry.Result.onPremisesSecurityIdentifier == null)
                                        continue;

                                    Count++;
                                    try
                                    {
                                        var _strId = Entry.ID;
                                        if (!Guid.TryParse(_strId, out var Id))
                                            continue;

                                        var _sid = Entry.Result.onPremisesSecurityIdentifier.ToString();
                                        var sid = new SecurityIdentifier(_sid);
                                        var arrSid = new byte[sid.BinaryLength];
                                        sid.GetBinaryForm(arrSid, 0);

                                        var SqlParams = new Dictionary<string, object>(2)
                                        {
                                            { "sid_bin", arrSid },
                                            { "id_azure", Id },
                                            { "tenant", Tenant.TenantID }
                                        };

                                        var _res = await DataHistorySqlHelper.ExecuteAsync(Conn, QueryInsert, SqlParams, requestInfo, Token);
                                        if (_res >= 0)
                                            Updated++;
                                    }
                                    catch (Exception E)
                                    {
                                        LogException(E);
                                    }
                                }
                                Collector.DoProcessUiMessage(2, $"{Count} accounts processed, {Updated} updated.");

                                if (Result.Count < constMsGraphUserPaging)
                                    break;

                                Result.Clear();
                                if (!await MsGraph.RequestNextAsync(Result))
                                    break;
                            }
                        }
                        catch (Exception E)
                        {
                            LogException(E);
                        }

                        Collector.DoProcessUiMessage(1, $"...scanning tenat {Tenant.Domain} finished");
                    }

                    Collector.DoProcessUiMessage(0, "... Azure cloud scans finished.");

                    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;
        }

        private async Task TestAzureAccessItem(cMsGraphBase MsGraph, bool UseBeta, string strUrlList, string strUrlEntry, string MessageList, string MessageEntry, CancellationToken Token, string nameProperty = null)
        {
            var strUrl = string.Format(strUrlList, 999);
            var ResultAdList = await MsGraph.RequestListAsync(strUrl, UseBeta: UseBeta, retryForbidden: false, loadPaged: false);
            if (ResultAdList != null && ResultAdList.Count > 0)
            {
                Collector.DoProcessUiMessage(1, $"  {ResultAdList.Count} {MessageList} found.");
                var _rnd = new Random();
                int p = _rnd.Next(ResultAdList.Count - 1);
                var _id = ResultAdList[p].ID;
                if (!string.IsNullOrEmpty(_id))
                {
                    strUrl = string.Format(strUrlEntry, _id);
                    var ResultAdEntry = await MsGraph.RequestAsync(strUrl, UseBeta: UseBeta, retryForbidden: false, nameProperty: nameProperty);
                    if (ResultAdEntry != null)
                    {
                        var _ty = ResultAdEntry.Result.GetType();
                        var _props = ResultAdEntry.Result as JObject;
                        if (_props != null)
                        {
                            Collector.DoProcessUiMessage(3, $"  properties of random selected {MessageEntry} '{ResultAdEntry.DisplayName}' with {_props.Count} properties could be read.");
                        }
                    }
                }
            }
            else
            {
                Collector.DoProcessUiMessage(1, $"  warning: no {MessageList} could be found.", LogLevels.Warning);
            }
        }

        public async Task TestAzureAccess(CancellationToken Token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            try
            {
                var tenants = Collector.InfrastructureConfig.AzureTenants?.Values;
                if (tenants is null)
                {
                    Collector.DoProcessUiMessage(2, $" could not logon to tenant - Tenant is null ", LogLevels.Error);
                    return;
                }

                foreach (var Tenant in tenants)
                {
                    // logon to tenant
                    Collector.DoProcessUiMessage(0, $"test scanning tenat {Tenant.Domain} for users, devices and managed devices ...");
                    var MsGraph = await GetGraphForTenantAsync(Tenant.TenantID, true);
                    if (MsGraph == null)
                    {

                        Collector.DoProcessUiMessage(2, $"error: could not logon to tenat {Tenant.Domain}", LogLevels.Error);
                        continue;
                    }

                    await TestAzureAccessItem(MsGraph, false, constMsGraphUserList, constMsGraphUserEntry, "synchronized user accounts", "user", Token);
                    await TestAzureAccessItem(MsGraph, false, constMsGraphDeviceList, constMsGraphDeviceEntry, "synchronized device accounts", "device", Token);
                    await TestAzureAccessItem(MsGraph, true, constMsGraphManagedDeviceList, constMsGraphManagedDeviceEntry, "Intune managed Windows device accounts", "Windows device", Token, nameProperty: "deviceName");

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



        }

        public async Task<bool> DoDeviceScanAzureAsync(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 tenants = Collector.InfrastructureConfig.AzureTenants?.Values;
                if (tenants is null)
                {
                    return true;
                }

                if (!DataHistorySqlHelper.GetWellKnownSqlStatement("UpdateComputerAzure", out var QueryInsert))
                    return false;

                using (var Conn = new cDbConnection(Collector.mainDbConnection))
                {
                    if (!Conn.IsOpen)
                    {
                        LogEntry($"Could not open main sql database '{Collector.mainDbConnection.Database}', aborting scan...'", LogLevels.Error);
                        return false;
                    }

                    foreach (var Tenant in tenants)
                    {
                        try
                        {
                            if (Tenant.ScanIntuneDevices)
                            {
                                // logon to tenant
                                Collector.DoProcessUiMessage(1, $"scanning tenat {Tenant.Domain} for devices ...");
                                var MsGraph = await GetGraphForTenantAsync(Tenant.TenantID, true);
                                if (MsGraph == null)
                                {
                                    Collector.DoProcessUiMessage(2, $"error: could not logon to tenat {Tenant.Domain}", LogLevels.Error);
                                    continue;
                                }

                                var strUrl = string.Format(constMsGraphDeviceList, constMsGraphDevicePaging);
                                var Result = await MsGraph.RequestListAsync(strUrl, UseBeta: false, retryForbidden: false, loadPaged: true);

                                var Count = 0;
                                var Updated = 0;
                                while (Result != null == Result.Count > 0)
                                {
                                    foreach (var Entry in Result)
                                    {
                                        if (Entry.Result.onPremisesSecurityIdentifier == null)
                                            continue;

                                        Count++;
                                        try
                                        {
                                            var _strId = Entry.ID;
                                            if (!Guid.TryParse(_strId, out var Id))
                                                continue;

                                            string _dvcId = Entry.Result.deviceId;
                                            if (!Guid.TryParse(_dvcId, out var deviceId))
                                                continue;

                                            var _sid = Entry.Result.onPremisesSecurityIdentifier.ToString();
                                            var sid = new SecurityIdentifier(_sid);
                                            var arrSid = new byte[sid.BinaryLength];
                                            sid.GetBinaryForm(arrSid, 0);

                                            var SqlParams = new Dictionary<string, object>(2)
                                         {
                                             { "sid_bin", arrSid },
                                             { "id", Id },
                                             { "deviceid", deviceId },
                                             { "tenant", Tenant.TenantID }
                                         };

                                            var _res = await DataHistorySqlHelper.ExecuteAsync(Conn, QueryInsert, SqlParams, requestInfo, Token);
                                            if (_res > 0)
                                                Updated++;
                                        }
                                        catch (Exception E)
                                        {
                                            LogException(E);
                                        }
                                    }
                                    Collector.DoProcessUiMessage(2, $"{Count} devices processed, {Updated} updated.");

                                    if (Result.Count < constMsGraphManagedDevicePaging)
                                        break;

                                    Result.Clear();
                                    if (!await MsGraph.RequestNextAsync(Result))
                                        break;
                                }
                            }
                        }
                        catch (Exception E)
                        {
                            LogException(E);
                        }
                    }
                    Collector.DoProcessUiMessage(0, "... Device cloud scans finished.");
                }
            }
            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;
        }

        private async Task SetVitalSignInfo(cDbConnection Conn, Guid AzureDeviceID, DateTime LastIntuneSync, Guid Tenant, Guid IntuneID, string Name, string UserName, 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;

            if (!DataHistorySqlHelper.GetWellKnownSqlStatement("GetComputerVitalSignInfoForIntune", out var QueryGetVitalSignInfo))
                return;

            if (!DataHistorySqlHelper.GetWellKnownSqlStatement("UpdateVitalSignMonitoringAll", out var QueryUpdateVitalSignInfoAll))
                return;

            if (!DataHistorySqlHelper.GetWellKnownSqlStatement("UpdateVitalSignMonitoringIntune", out var QueryUpdateVitalSignInfoIntune))
                return;

            try
            {
                // try getting the F4SD device id and the lastSeenByAgent info for the given AzureDeviceID
                var SqlParams = new Dictionary<string, object>(1)
                {
                    { "AZURE_ID", AzureDeviceID }
                };

                Guid _id = Guid.Empty;
                DateTime? lastSeen = null;
                using (var _reader = await DataHistorySqlHelper.GetDataReaderAsync(Conn, QueryGetVitalSignInfo, SqlParams, Token))
                {
                    if (_reader != null)
                        try
                        {
                            if (_reader.HasRows)
                            {
                                if (await _reader.ReadAsync())
                                {
                                    try
                                    {
                                        _id = _reader.GetGuid(0);
                                        lastSeen = _reader.IsDBNull(1) ? (DateTime?)null : _reader.GetDateTime(1);
                                    }
                                    catch (Exception E)
                                    {
                                        cLogManager.DefaultLogger.LogException(E);
                                    }
                                }
                            }
                        }
                        catch (Exception E)
                        {
                            cLogManager.DefaultLogger.LogException(E);
                        }
                }

                if (_id == Guid.Empty)
                {
                    SqlParams = new Dictionary<string, object>(3)
                    {
                        { "@TenantId", Tenant },
                        { "@IntuneId", IntuneID },
                        { "@name", Name },
                        { "@AzureID", AzureDeviceID },
                        { "@UserName", UserName },
                        { "@TimeStamp", LastIntuneSync }
                    };

                    await DataHistorySqlHelper.ExecuteAsync(Conn, QueryUpdateVitalSignInfoIntune, SqlParams, requestInfo, Token);

                    return;
                }

                // update the vital sign info of the device found
                int diffToAgent = int.MinValue;
                if (lastSeen != null)
                    diffToAgent = (int)(LastIntuneSync - lastSeen.Value).TotalHours;
                if (diffToAgent == 0)
                {

                }
                SqlParams = new Dictionary<string, object>(3)
                {
                    { "@Id", _id },
                    { "@TimeStamp", LastIntuneSync },
                    { "@DiffToAgent", diffToAgent },
                    { "@UserName", UserName },
                };

                await DataHistorySqlHelper.ExecuteAsync(Conn, QueryUpdateVitalSignInfoAll, SqlParams, requestInfo, 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);
            }
        }

        public async Task<bool> DoScanIntuneAsync(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 tenants = Collector.InfrastructureConfig.AzureTenants?.Values;
                if (tenants is null)
                {
                    return true;
                }

                if (!DataHistorySqlHelper.GetWellKnownSqlStatement("UpdateComputerIntune", out var QueryInsert))
                    return false;

                using (var Conn = new cDbConnection(Collector.mainDbConnection))
                {
                    if (!Conn.IsOpen)
                    {
                        LogEntry($"Could not open main sql database '{Collector.mainDbConnection.Database}', aborting scan...'", LogLevels.Error);
                        return false;
                    }

                    bool HasTenantToScan = false;
                    foreach (var Tenant in tenants)
                    {
                        try
                        {
                            if (Tenant.ScanIntuneDevices)
                            {
                                HasTenantToScan = true;
                                // logon to tenant
                                Collector.DoProcessUiMessage(1, $"scanning tenat {Tenant.Domain} for devices in Intune ...");
                                var MsGraph = await GetGraphForTenantAsync(Tenant.TenantID, true);
                                if (MsGraph == null)
                                {
                                    Collector.DoProcessUiMessage(2, $"error: could not logon to tenat {Tenant.Domain}", LogLevels.Error);
                                    continue;
                                }

                                var strUrl = string.Format(constMsGraphManagedDeviceList, constMsGraphManagedDevicePaging);
                                var Result = await MsGraph.RequestListAsync(strUrl, UseBeta: true, retryForbidden: false, loadPaged: true);

                                var Count = 0;
                                var Updated = 0;
                                while (Result != null == Result.Count > 0)
                                {
                                    foreach (var Entry in Result)
                                    {

                                        Count++;
                                        try
                                        {
                                            var _strId = Entry.ID;
                                            if (!Guid.TryParse(_strId, out var Id))
                                                continue;

                                            string _dvcId = Entry.Result.azureActiveDirectoryDeviceId;
                                            if (!Guid.TryParse(_dvcId, out var deviceId))
                                                continue;


                                            var SqlParams = new Dictionary<string, object>(2)
                                            {
                                                { "id", Id },
                                                { "deviceid", deviceId }
                                            };

                                            var _res = await DataHistorySqlHelper.ExecuteAsync(Conn, QueryInsert, SqlParams, requestInfo, Token);
                                            if (_res > 0)
                                                Updated++;
                                            else
                                            {
                                                if (DataHistorySqlHelper.GetWellKnownSqlStatement("UpdateComputerIntune2", out var QueryInsert2))
                                                {
                                                    string _dvcName = Entry.Result.deviceName;
                                                    var SqlParams2 = new Dictionary<string, object>(2)
                                                                     {
                                                                         { "id", Id },
                                                                         { "deviceid", deviceId },
                                                                         { "tenant", Tenant.TenantID },
                                                                         { "upn_internal", (_dvcName+"$@"+Tenant.TenantID).ToLowerInvariant() }
                                                                     };
                                                    var _res2 = await DataHistorySqlHelper.ExecuteAsync(Conn, QueryInsert2, SqlParams2, requestInfo, Token);
                                                    if (_res2 > 0)
                                                        Updated++;
                                                }
                                            }

#if VITALSIGN_MONITORING
                                            String Name = Entry.Result.deviceName;
                                            String UserName = Entry.Result.userPrincipalName;

                                            if (Entry.Result.lastSyncDateTime is JValue proplastSyncTime)
                                            {
                                                var lastSyncTime = proplastSyncTime.ToObject<DateTime>();
                                                await SetVitalSignInfo(Conn, deviceId, lastSyncTime, Tenant.TenantID, Id, Name, UserName, requestInfo, LogDeep + 1, Token);
                                            }
#endif
                                        }
                                        catch (Exception E)
                                        {
                                            LogException(E);
                                        }
                                    }
                                    Collector.DoProcessUiMessage(2, $"{Count} devices processed, {Updated} updated.");

                                    if (Result.Count < constMsGraphManagedDevicePaging)
                                        break;

                                    Result.Clear();
                                    if (!await MsGraph.RequestNextAsync(Result))
                                        break;
                                }
                            }
                        }
                        catch (Exception E)
                        {
                            LogException(E);
                        }
                    }

                    if (HasTenantToScan)
                        Collector.DoProcessUiMessage(0, "... Intune cloud scans finished.");
                }
            }
            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 async Task<cF4sdStagedSearchResultRelations> GetRelationsAsync(IEnumerable<cF4sdConnectorIds> ids, enumFasdInformationClass informationClass, int age, CancellationToken token = default)
        {
            try
            {
                /*
                List<cF4sdConnectorIds> Ids = await _collector.getConnectorIdList(ids.ToList(), token, null, 0);
                List<cF4sdApiSearchResultRelation> relations = await GetUserMobileDevicesAsync(Ids, token, null);
                return new cF4sdStagedSearchResultRelations() { Relations = relations };
                */
                return null;
            }
            catch (Exception ex)
            {
                LogException(ex);
            }

            return null;
        }
        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
            {

                var bDeviceExists = Tables.Exists(v => v.Name == constTableNameIntuneDeviceDetails && v.ParentCluster?.Origin == enumDataHistoryOrigin.Intune);
                var bUserDeviceExists = Tables.Exists(v => v.Name == constTableNameIntuneUserDeviceCount && v.ParentCluster?.Origin == enumDataHistoryOrigin.Intune);
                var bMobileDeviceExists = Tables.Exists(v => v.Name == constTableNameIntuneMobileDeviceDetails && v.ParentCluster?.Origin == enumDataHistoryOrigin.Intune);


                if (!Identities.TryGetValue(enumFasdInformationClass.User, out var user) && user?.intuneId != Guid.Empty && user?.tenantId != Guid.Empty)
                    bUserDeviceExists = false;

                if (!Identities.TryGetValue(enumFasdInformationClass.Computer, out var computer) && computer?.intuneId != Guid.Empty && user?.tenantId != Guid.Empty)
                    bDeviceExists = false;

                if (!Identities.TryGetValue(enumFasdInformationClass.MobileDevice, out var mobile) && mobile?.Id != Guid.Empty)
                    bMobileDeviceExists = false;




                if (!(bDeviceExists || bMobileDeviceExists || bUserDeviceExists))
                    return null;

                var retVal = new List<cF4SDHealthCardRawData.cHealthCardTable>();
                var listTasks = new List<Task<List<cF4SDHealthCardRawData.cHealthCardTable>>>(2);

                if (bDeviceExists)
                    if (CacheId == null && !instantly)
                    {
                        retVal.Add(new cF4SDHealthCardRawData.cHealthCardTable()
                        {
                            Name = constTableNameIntuneDeviceDetails,
                            InformationClass = enumFasdInformationClass.Computer,
                            Origin = enumDataHistoryOrigin.Intune,
                            IsIncomplete = true
                        });

                    }
                    else
                        listTasks.Add(Task.Run<List<cF4SDHealthCardRawData.cHealthCardTable>>(async () => { return await GetIntuneDeviceTableAsync(computer.intuneId, user.tenantId, requestInfo, LogDeep + 1, Token); }));

                if (bMobileDeviceExists)
                    if (CacheId == null && !instantly)
                    {
                        retVal.Add(new cF4SDHealthCardRawData.cHealthCardTable()
                        {
                            Name = constTableNameIntuneMobileDeviceDetails,
                            InformationClass = enumFasdInformationClass.MobileDevice,
                            Origin = enumDataHistoryOrigin.Intune,
                            IsIncomplete = true
                        });

                    }
                    else
                        listTasks.Add(Task.Run<List<cF4SDHealthCardRawData.cHealthCardTable>>(async () => { return await GetIntuneMobileDeviceTableAsync(mobile.Id, user.tenantId, requestInfo, LogDeep + 1, Token); }));

                if (bUserDeviceExists)
                    if (CacheId == null && !instantly)
                    {
                        retVal.Add(new cF4SDHealthCardRawData.cHealthCardTable()
                        {
                            Name = constTableNameIntuneUserDeviceCount,
                            InformationClass = enumFasdInformationClass.User,
                            Origin = enumDataHistoryOrigin.Intune,
                            IsIncomplete = true
                        });
                    }
                    else
                        listTasks.Add(Task.Run<List<cF4SDHealthCardRawData.cHealthCardTable>>(async () => { return await GetIntuneUserDeviceCountTableAsync(user.intuneId, user.tenantId, requestInfo, LogDeep + 1, Token); }));

                var arrRes = await Task.WhenAll(listTasks.ToArray());
                foreach (var Entry in arrRes)
                    if (Entry != null)
                        retVal.AddRange(Entry);


                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;
        }

        public override async Task<List<cF4SDHealthCardRawData.cHealthCardDetailsTable>> GetDetailsTableResultsVirtualAsync(List<cDataHistoryConfigTable> Tables, Dictionary<enumFasdInformationClass, cF4sdConnectorIds> Identities, DateTime RefTime, int MaxAge, 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
            {
                var bUserDeviceDetailExists = Tables.Exists(v => v.Name == constTableNameIntuneUserDeviceCountDetail && v.ParentCluster?.Origin == enumDataHistoryOrigin.Intune);

                if (!Identities.TryGetValue(enumFasdInformationClass.User, out var userIds))
                {

                    bUserDeviceDetailExists = false;
                }



                if (!bUserDeviceDetailExists)
                    return null;

                var listTasks = new List<Task<List<cF4SDHealthCardRawData.cHealthCardDetailsTable>>>(2);

                if (bUserDeviceDetailExists)
                    listTasks.Add(Task.Run<List<cF4SDHealthCardRawData.cHealthCardDetailsTable>>(async () => { return await GetIntuneUserDeviceCountDetailTableAsync(userIds.intuneId, userIds.tenantId, requestInfo, LogDeep + 1, Token); }));


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


                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>> GetIntuneDeviceTableAsync(Guid IntuneId, Guid TenantId, 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(constTableNameIntuneDeviceDetails, out var IntuneTableDetails))
                    return null;

                var strUrl = string.Format(constMsGraphManagedDeviceEntry, IntuneId);

                var MsGraph = await GetGraphForTenantAsync(TenantId, true);
                if (MsGraph == null)
                    return null;

                var retVal = new cF4SDHealthCardRawData.cHealthCardTable()
                {
                    Name = constTableNameIntuneDeviceDetails,
                    InformationClass = enumFasdInformationClass.Computer,
                    Origin = enumDataHistoryOrigin.Intune,
                    IsStatic = true,
                    TableType = eDataHistoryTableType.Static
                };

                var dicVals = new Dictionary<string, object>();

                var res = await MsGraph.RequestAsync(strUrl, UseBeta: true, retryForbidden: false);
                if (res != null)
                {
                    JObject deviceInfos = res.Result;

                    var vals = deviceInfos.Children();

                    if (cLogManager.DefaultLogger.IsDebug) LogEntry("SpecialDebug GetIntuneMainTableAsync #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 intune detail property '{token.Path}'", LogLevels.Error);
                            LogException(E);
                        }

                    }

                    foreach (var colInfo in IntuneTableDetails.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 } };
                            retVal.Columns[colInfo.Name] = _col;
                        }
                        catch { }
                    }
                    return new List<cF4SDHealthCardRawData.cHealthCardTable>(1) { retVal };
                }
                else
                {
                    LogEntry($"Could not get detailed device information from intune with id {IntuneId}. Device content is empty.", LogLevels.Warning);
                    foreach (var colInfo in IntuneTableDetails.Columns.Values)
                    {
                        try
                        {
                            var _col = new cF4SDHealthCardRawData.cHealthCardTableColumn(retVal) { ColumnName = colInfo.Name, Values = new List<object>(1) { } };
                            retVal.Columns[colInfo.Name] = _col;
                        }
                        catch { }
                    }
                    return new List<cF4SDHealthCardRawData.cHealthCardTable>(1) { 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>> GetIntuneUserDeviceCountTableAsync(Guid Userid, Guid TenantId, 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(constTableNameIntuneUserDeviceCount, out var IntuneTableDetails))
                    return null;

                var strUrl = string.Format(constMsGraphManagedDeviceCount, Userid);
                if (cLogManager.DefaultLogger.IsDebug) LogEntry("SpecialDebug  GetIntuneUserDeviceCountTableAsync #1");
                var MsGraph = await GetGraphForTenantAsync(TenantId, true);
                if (cLogManager.DefaultLogger.IsDebug) LogEntry("SpecialDebug  GetIntuneUserDeviceCountTableAsync #2");
                if (MsGraph == null)
                    return null;

                if (cLogManager.DefaultLogger.IsDebug) LogEntry("SpecialDebug  GetIntuneUserDeviceCountTableAsync #3");
                var res = await MsGraph.RequestAsync(strUrl, UseBeta: false, retryForbidden: false);
                if (cLogManager.DefaultLogger.IsDebug) LogEntry("SpecialDebug  GetIntuneUserDeviceCountTableAsync #4");

                var retVal = new cF4SDHealthCardRawData.cHealthCardTable()
                {
                    Name = constTableNameIntuneUserDeviceCount,
                    InformationClass = enumFasdInformationClass.User,
                    Origin = enumDataHistoryOrigin.Intune,
                    IsStatic = true,
                    TableType = eDataHistoryTableType.Static
                };

                if (res != null)
                {
                    JObject deviceInfos = res.Result;

                    var vals = deviceInfos.Children();

                    var devices = deviceInfos["value"]?
                   .Select(d => (JObject)d)
                   .GroupBy(d => (string)d["operatingSystem"])
                   .ToDictionary(g => g.Key, g => (object)g.Count()).ToList();

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

                    var dicVals = new Dictionary<string, object>
                    {
                        { "id", Userid },
                        { "windows", 0 },
                        { "iOS", 0 },
                        { "android", 0 },
                        { "linux", 0 },
                        { "macOs", 0 },
                        { "total", 0 }
                    };

                    if (devices != null)
                    {

                        dicVals["total"] = devices.Sum(kvp => (int)kvp.Value);

                        foreach (var value in devices)
                        {
                            switch (value.Key.ToLower())
                            {
                                case "windows":
                                    dicVals["windows"] = value.Value;
                                    break;
                                case "ios":
                                    dicVals["iOS"] = value.Value;
                                    break;
                                case "android":
                                    dicVals["android"] = value.Value;
                                    break;
                                case "linux":
                                    dicVals["linux"] = value.Value;
                                    break;
                                case "macos":
                                    dicVals["macOs"] = value.Value;
                                    break;
                            }

                        }
                        foreach (var colInfo in IntuneTableDetails.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 } };
                                retVal.Columns[colInfo.Name] = _col;
                            }
                            catch { }
                        }
                    }
                    return new List<cF4SDHealthCardRawData.cHealthCardTable>(1) { retVal };
                }
                else
                {
                    LogEntry($"Could not get detailed DeviceCount information from intune with id {Userid}. DeviceCount content is empty.", LogLevels.Warning);
                    foreach (var colInfo in IntuneTableDetails.Columns.Values)
                    {
                        try
                        {
                            var _col = new cF4SDHealthCardRawData.cHealthCardTableColumn(retVal) { ColumnName = colInfo.Name, Values = new List<object>(1) { } };
                            retVal.Columns[colInfo.Name] = _col;
                        }
                        catch { }
                    }
                    return new List<cF4SDHealthCardRawData.cHealthCardTable>(1) { 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.cHealthCardDetailsTable>> GetIntuneUserDeviceCountDetailTableAsync(Guid Userid, Guid TenantId, 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(constTableNameIntuneUserDeviceCountDetail, out var IntuneTableDetails))
                    return null;

                var strUrl = string.Format(constMsGraphManagedUserDeviceEntry, Userid);

                // logon to tenant
                var MsGraph = await GetGraphForTenantAsync(TenantId, true);
                if (MsGraph == null)
                    return null;
                var retValList = new List<cF4SDHealthCardRawData.cHealthCardDetailsTable>();
                var res = await MsGraph.RequestAsync(strUrl, UseBeta: false, retryForbidden: false);

                var retVal = new cF4SDHealthCardRawData.cHealthCardDetailsTable()
                {
                    Name = constTableNameIntuneUserDeviceCountDetail,
                    Columns = IntuneTableDetails.Columns.Values.Select(v => v.Name).ToList(),
                    Values = new Dictionary<int, List<object[]>>()

                };


                if (res != null)
                {
                    JObject deviceInfos = res.Result;

                    var vals = deviceInfos["value"] as JArray;
                    if (vals != null)
                    {


                        int index = 0;
                        foreach (var device in vals)
                        {
                            try
                            {
                                var dicVals = device.Children<JProperty>().ToDictionary(prop => prop.Name, prop => prop.Value?.ToObject<object>());

                                var row = retVal.Columns.Select(colName => dicVals.TryGetValue(colName, out var value) ? value : null).ToArray();

                                if (!retVal.Values.ContainsKey(0))
                                {
                                    retVal.Values[0] = new List<object[]>();
                                }
                                retVal.Values[0].Add(row);
                            }
                            catch (Exception e)
                            {
                                LogEntry($"Error processing device entry '{device.Path}'", LogLevels.Error);
                                LogException(e);
                            }
                            index++;
                        }

                        retValList.Add(retVal);
                        return retValList;
                    }
                    else
                    {
                        LogEntry($"Could not get detailed DeviceCount information from intune with id {Userid}. DeviceCount content is empty.", LogLevels.Warning);
                        retValList.Add(retVal);
                        return retValList;
                    }
                }
                return null;
            }
            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<List<cF4sdApiSearchResultRelation>> GetUserMobileDevicesAsync(List<cF4sdConnectorIds> UserId, CancellationToken Token, cF4sdWebRequestInfo requestInfo)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            try
            {
                string userIds = string.Join(",", UserId.Where(u => !string.IsNullOrWhiteSpace(u?.intuneId.ToString())).Select(u => u.intuneId));

                var tenantId = UserId?.FirstOrDefault(u => u?.tenantId != Guid.Empty)?.tenantId ?? Guid.Empty;

                var strUrl = string.Format(constMsGraphManagedMobileDeviceList, userIds);

                var MsGraph = await GetGraphForTenantAsync(tenantId, true);
                if (MsGraph == null)
                    return null;

                var Result = await MsGraph.RequestListAsync(strUrl, UseBeta: false, retryForbidden: false);
                if (Result != null == Result.Count > 0)
                {
                    var searchResult = new List<cF4sdApiSearchResultRelation>(Result.Count);

                    foreach (var Entry in Result)
                    {
                        var endDate = DateTime.MinValue;
                        if (!DateTime.TryParse(Entry.Result.lastSyncDateTime?.ToString(), out endDate))
                            endDate = DateTime.UtcNow;

                        var searchInfo = new cF4sdApiSearchResultRelation()
                        {
                            Type = enumF4sdSearchResultClass.MobileDevice,
                            Name = Entry.Result.deviceName?.ToString(),
                            DisplayName = Entry.Result.deviceName?.ToString(),
                            LastUsed = endDate,
                            UsingLevel = 0,
                            id = Entry.Result.id,
                            Status = enumF4sdSearchResultStatus.Active,
                            Identities = new cF4sdIdentityList()
                            {
                                new cF4sdIdentityEntry(){ Class = enumFasdInformationClass.MobileDevice, Id = Entry.Result?.id},
                                new cF4sdIdentityEntry(){ Class = enumFasdInformationClass.User, Id = (Entry.Result.userId == null || Entry.Result?.userId == DBNull.Value) ?  UserId[0].Id : Entry.Result.userId }
                            }
                        };
                        searchResult.Add(searchInfo);
                    }
                    //if (searchResult != null && searchResult.Count > 0)
                    //{
                    //    foreach (var Entry in searchResult)
                    //    {
                    //        Entry.UsingLevel /= _sumDuration;
                    //    }
                    //}
                    return searchResult;
                }

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

            return null;
        }

        private async Task<List<cF4SDHealthCardRawData.cHealthCardTable>> GetIntuneMobileDeviceTableAsync(Guid IntuneId, Guid TenantId, 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(constTableNameIntuneMobileDeviceDetails, out var IntuneTableDetails))
                    return null;

                var strUrl = string.Format(constMsGraphManagedDeviceAllEntry, IntuneId);

                var MsGraph = await GetGraphForTenantAsync(TenantId, true);
                if (MsGraph == null)
                    return null;

                var res = await MsGraph.RequestAsync(strUrl, UseBeta: true, retryForbidden: false);
                if (res != null)
                {
                    JObject deviceInfos = res.Result;
                    var retVal = new cF4SDHealthCardRawData.cHealthCardTable()
                    {
                        Name = constTableNameIntuneMobileDeviceDetails,
                        InformationClass = enumFasdInformationClass.MobileDevice,
                        Origin = enumDataHistoryOrigin.Intune,
                        IsStatic = true,
                        TableType = eDataHistoryTableType.Static
                    };

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

                    if (cLogManager.DefaultLogger.IsDebug) LogEntry("SpecialDebug GetIntuneMainTableAsync #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 intune detail property '{token.Path}'", LogLevels.Error);
                            LogException(E);
                        }

                    }

                    foreach (var colInfo in IntuneTableDetails.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 } };
                            retVal.Columns[colInfo.Name] = _col;
                        }
                        catch { }
                    }
                    return new List<cF4SDHealthCardRawData.cHealthCardTable>(1) { retVal };
                }
                else
                {
                    LogEntry($"Could not get detailed device information from intune with id {IntuneId}. Device content is empty.", LogLevels.Warning);
                }
            }
            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> QuickActionRun(cF4SDServerQuickActionParameters jsonRequest, cF4sdWebRequestInfo requestInfo, CancellationToken Token)
        {

            dynamic result = null;
            switch (jsonRequest.Action)
            {
                case "UpdateComplianceState":
                    result = await UpdateComplianceState(jsonRequest.Identities, requestInfo, CancellationToken.None);
                    break;
                case "ChangeManagedStatus":
                    result = await ChangeManagedStatus(jsonRequest.Identities, requestInfo, jsonRequest.AdjustableParameter?.Values?.FirstOrDefault()?.ToString() ?? string.Empty, CancellationToken.None);
                    break;
                case "GetManagedApps":
                    result = await GetManagedApps(jsonRequest.Identities, requestInfo, CancellationToken.None);
                    break;
                case "GetBitlockerKey":
                    result = await GetBitlockerKey(jsonRequest.Identities, requestInfo, CancellationToken.None);
                    break;
                case "GetLAPS":
                    result = await GetLAPS(jsonRequest.Identities, requestInfo, CancellationToken.None);
                    break;
                case "GetTAP":
                    result = await GetTAP(jsonRequest.Identities, requestInfo, CancellationToken.None);
                    break;
                case "GetBitlockerKeyTest":
                    result = await GetBitlockerKeyTest(jsonRequest.Identities, requestInfo, CancellationToken.None);
                    break;
                case "GetLapsKeyTest":
                    result = await GetLapsKeyTest(jsonRequest.Identities, requestInfo, CancellationToken.None);
                    break;
            }
            return result;
        }

        public override async Task<List<string>> GetQuickActionList()

        {
            var HasIntuneConfig = (Collector.InfrastructureConfig.AzureTenants?.Values.Any(t => t.ScanIntuneDevices) == true);
            if (HasIntuneConfig)
            {
                var quickactions = new List<string>{
                "Update ComplianceState",
                "Change ManagedStatus",
                "Get Managed Apps",
                "Get BitlockerKey",
                "Get LAPS",
                "Get TAP",
                "GetLapsKeyTest",
            };
                return await Task.FromResult(quickactions);
            }
            var result = new List<string>();
            return result;
        }

        public async Task<dynamic> UpdateComplianceState(List<cF4sdIdentityEntry> Identities, cF4sdWebRequestInfo requestInfo, CancellationToken Token)
        {
            var Ids = await Collector.getConntectorIds(Identities, Token, requestInfo, 1);
            if (!Ids.TryGetValue(enumFasdInformationClass.Computer, out var _ids))
                return null;

            var strUrl = string.Format(constMsGraphManagedDeviceSyncDevice, _ids.intuneId);

            var MsGraph = await GetGraphForTenantAsync(_ids.tenantId, true);
            if (MsGraph == null)
            {
                return null;
            }

            var Result = await MsGraph.RequestAsync(strUrl, UseBeta: false, httpMethod: eHttpMethod.post, retryForbidden: false);
            if (Result != null)
            {

                var ResultJson = Result.Result as JObject;
                if (ResultJson != null)
                {
                    Collector.DoProcessUiMessage(3, $"  properties of selected '{ResultJson}' with {ResultJson.Count} properties could be read.");
                }

                return ResultJson;

            }
            return null;

        }

        public async Task<dynamic> GetBitlockerKey(List<cF4sdIdentityEntry> identities, cF4sdWebRequestInfo requestInfo, CancellationToken Token)
        {
            var Ids = await Collector.getConntectorIds(identities, Token, requestInfo, 1);
            if (!Ids.TryGetValue(enumFasdInformationClass.Computer, out var _ids))
                return null;

            // URL zum Abrufen der Recovery-Keys
            var strUrl = string.Format(constMsGraphDeviceBitlockerRecoveryKey, _ids.azureDeviceId);

            var MsGraph = await GetGraphForTenantAsync(_ids.tenantId, true);
            if (MsGraph == null)
                return null;

            // Schritt 1: Abrufen der Recovery-Key IDs
            var resultRecoveryKeys = await MsGraph.RequestAsync(strUrl, UseBeta: false, retryForbidden: false);

            if (resultRecoveryKeys?.Result["value"] is JArray recoveryKeyArray && recoveryKeyArray.Count > 0)
            {
                var results = new JArray();

                foreach (var item in recoveryKeyArray)
                {
                    var recoveryKeyId = item["id"].ToString();

                    // Schritt 2: Abrufen des Recovery-Key-Wertes
                    var strUrl2 = string.Format(constMsGraphDeviceBitlockerRecoveryKeyDetails, recoveryKeyId);
                    var resultKey = await MsGraph.RequestAsync(strUrl2, UseBeta: false, retryForbidden: false);

                    if (resultKey?.Result != null)
                    {
                        results.Add(resultKey.Result as JObject);
                    }
                }

                Collector.DoProcessUiMessage(3, $"{results.Count} Bitlocker-Key(s) erfolgreich abgerufen.");

                return results;
            }

            Collector.DoProcessUiMessage(3, "Kein Bitlocker-Key gefunden oder Abruf fehlgeschlagen.");

            return null;
        }

        public async Task<dynamic> GetLAPS(List<cF4sdIdentityEntry> identities, cF4sdWebRequestInfo requestInfo, CancellationToken Token)
        {
            var Ids = await Collector.getConntectorIds(identities, Token, requestInfo, 1);
            if (!Ids.TryGetValue(enumFasdInformationClass.Computer, out var _ids))
                return null;

            var strUrl = string.Format(constMsGraphDeviceLAPS, _ids.azureDeviceId);


            var MsGraph = await GetGraphForTenantAsync(_ids.tenantId, true);
            if (MsGraph == null)
                return null;

            var Result = await MsGraph.RequestAsync(strUrl, UseBeta: false, retryForbidden: false);
            if (Result != null)
            {
                var _resultJson = Result.Result["credentials"] as JArray;
                if (_resultJson != null)
                {
                    try
                    {
                        var ResultJson = _resultJson.ToObject<List<JObject>>().GroupBy(obj => (string)obj["accountName"])
                                         .Select(group => group.OrderByDescending(obj => DateTimeOffset.Parse((string)obj["backupDateTime"], CultureInfo.InvariantCulture)).First()).ToList();

                        foreach (JObject item in ResultJson)
                        {
                            string _base64Password = (string)item["passwordBase64"];
                            byte[] passwordBytes = Convert.FromBase64String(_base64Password);
                            string decodedPassword = Encoding.UTF8.GetString(passwordBytes);
                            item["passwordBase64"] = decodedPassword;

                        }
                        Collector.DoProcessUiMessage(3, $"  properties of selected '{ResultJson}' with {ResultJson.Count} properties could be read.");
                        return ResultJson;
                    }
                    catch (Exception E)
                    {
                        cLogManager.DefaultLogger.LogException(E);
                    }
                }
            }

            return null;
        }

        public async Task<dynamic> GetTAP(List<cF4sdIdentityEntry> identities, cF4sdWebRequestInfo requestInfo, CancellationToken Token)
        {
            var Ids = await Collector.getConntectorIds(identities, Token, requestInfo, 1);
            if (!Ids.TryGetValue(enumFasdInformationClass.User, out var _ids))
                return null;

            var strUrl = string.Format(constMsGraphTAP, _ids.intuneId);


            var MsGraph = await GetGraphForTenantAsync(_ids.tenantId, true);
            if (MsGraph == null)
                return null;

            var IntuneConfig = Collector?.GetGlobalConfig()?.IntuneConfiguration;

            var body = new Dictionary<string, object>()
                {
                    { "isUsableOnce", IntuneConfig?.TAPIsUsableOnce ?? cF4sdIntuneConfig.DefaultTAPIsUsableOnce},
                    { "lifetimeInMinutes", IntuneConfig?.TAPLifeTimeInMinutes ?? cF4sdIntuneConfig.DefaultTAPLifeTimeInMinutes}
                };

            var Result = await MsGraph.RequestAsync(strUrl, UseBeta: false, eHttpMethod.post, JsonData: body, retryForbidden: false);
            if (Result != null)
            {
                var _resultJson = Result.Result as JObject;
                if (_resultJson != null)
                {
                    return _resultJson;
                }
            }

            return null;
        }

        public async Task<dynamic> GetManagedApps(List<cF4sdIdentityEntry> identities, cF4sdWebRequestInfo requestInfo, CancellationToken Token)
        {
            var Ids = await Collector.getConntectorIds(identities, Token, requestInfo, 1);
            if (!Ids.TryGetValue(enumFasdInformationClass.Computer, out var _ids))
                return null;
            var strUrl = string.Format(constMsGraphManagedDeviceDetectedApps, _ids.intuneId);

            var MsGraph = await GetGraphForTenantAsync(_ids.tenantId, true);
            if (MsGraph == null)
                return null;

            var Result = await MsGraph.RequestAsync(strUrl, UseBeta: true, retryForbidden: false);
            if (Result != null)
            {
                var ResultJson = Result.Result["value"] as JArray;
                if (ResultJson != null)
                {
                    Collector.DoProcessUiMessage(3, $"  properties of selected '{ResultJson}' with {ResultJson.Count} properties could be read.");
                }

                return ResultJson;
            }

            return null;

        }

        public async Task<dynamic> ChangeManagedStatus(List<cF4sdIdentityEntry> identities, cF4sdWebRequestInfo requestInfo, string status, CancellationToken Token)
        {
            if (!string.IsNullOrWhiteSpace(status))
            {
                var Ids = await Collector.getConntectorIds(identities, Token, requestInfo, 1);
                if (!Ids.TryGetValue(enumFasdInformationClass.Computer, out var _ids))
                    return null;
                var strUrl = string.Format(constMsGraphManagedDeviceChangeManagedStatus, _ids.intuneId, status);

                var MsGraph = await GetGraphForTenantAsync(_ids.tenantId, true);
                if (MsGraph == null)
                    return null;

                var Result = await MsGraph.RequestAsync(strUrl, UseBeta: false, retryForbidden: false);
                if (Result != null)
                {
                    var ResultJson = Result.Result as JObject;
                    if (ResultJson != null)
                    {
                        Collector.DoProcessUiMessage(3, $"  properties of selected '{ResultJson}' with {ResultJson.Count} properties could be read.");

                    }
                    return ResultJson;

                }
            }
            return null;

        }


        public async Task<dynamic> GetBitlockerKeyTest(List<cF4sdIdentityEntry> identities, cF4sdWebRequestInfo requestInfo, CancellationToken Token)
        {
            var Ids = await Collector.getConntectorIds(identities, Token, requestInfo, 1);
            if (!Ids.TryGetValue(enumFasdInformationClass.Computer, out var _ids))
                return null;

            // URL zum Abrufen der Recovery-Keys
            var deviceId_str = "02f2fd68-a1c6-4e73-96a5-48f72792ee08";
            _ids.intuneId = new Guid(deviceId_str);
            var strUrl = string.Format(constMsGraphDeviceBitlockerRecoveryKey, _ids.intuneId);

            var MsGraph = await GetGraphForTenantAsync(_ids.tenantId, true);
            if (MsGraph == null)
                return null;

            // Schritt 1: Abrufen der Recovery-Key IDs
            var resultRecoveryKeys = await MsGraph.RequestAsync(strUrl, UseBeta: false, retryForbidden: false);

            if (resultRecoveryKeys?.Result["value"] is JArray recoveryKeyArray && recoveryKeyArray.Count > 0)
            {
                var results = new JArray();

                foreach (var item in recoveryKeyArray)
                {
                    var recoveryKeyId = item["id"].ToString();

                    // Schritt 2: Abrufen des Recovery-Key-Wertes
                    var strUrl2 = string.Format(constMsGraphDeviceBitlockerRecoveryKeyDetails, recoveryKeyId);
                    var resultKey = await MsGraph.RequestAsync(strUrl2, UseBeta: false, retryForbidden: false);

                    if (resultKey?.Result != null)
                    {
                        results.Add(resultKey.Result as JObject);
                    }
                }

                Collector.DoProcessUiMessage(3, $"{results.Count} Bitlocker-Key(s) erfolgreich abgerufen.");

                return results;
            }

            Collector.DoProcessUiMessage(3, "Kein Bitlocker-Key gefunden oder Abruf fehlgeschlagen.");

            return null;
        }

        public async Task<dynamic> GetLapsKeyTest(List<cF4sdIdentityEntry> identities, cF4sdWebRequestInfo requestInfo, CancellationToken Token)
        {
            var Ids = await Collector.getConntectorIds(identities, Token, requestInfo, 1);
            if (!Ids.TryGetValue(enumFasdInformationClass.Computer, out var _ids))
                return null;

            var azureId_str = "02f2fd68-a1c6-4e73-96a5-48f72792ee08";
            _ids.azureDeviceId = new Guid(azureId_str);

            var strUrl = string.Format(constMsGraphDeviceLAPS, _ids.azureDeviceId);

            var MsGraph = await GetGraphForTenantAsync(_ids.tenantId, true);
            if (MsGraph == null)
                return null;

            var Result = await MsGraph.RequestAsync(strUrl, UseBeta: false, retryForbidden: false);
            if (Result != null)
            {
                var ResultJson = Result.Result["credentials"] as JArray;
                if (ResultJson != null)
                {
                    foreach (JObject item in ResultJson)
                    {
                        string _base64Password = (string)item["passwordBase64"];
                        byte[] passwordBytes = Convert.FromBase64String(_base64Password);
                        string decodedPassword = Encoding.UTF8.GetString(passwordBytes);
                        item["passwordBase64"] = decodedPassword;

                    }


                    Collector.DoProcessUiMessage(3, $"  properties of selected '{ResultJson}' with {ResultJson.Count} properties could be read.");
                }
                JObject singleCredential = ResultJson[0] as JObject;
                return singleCredential;
            }

            return null;
        }

        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?.InfrastructureConfig?.ActiveDirectory?.Scans?.Count != null && Collector.InfrastructureConfig.ActiveDirectory.Scans.Count == 0)
                {
                    Collector.DoProcessUiMessage(0, "no AD scans defined, aborting AD scan.");
                    Collector.DoProcessUiMessage(0, "");
                    return false;
                }


                if (!Always)
                {
                    var scanInfo = await Collector.ActiveDirectory.GetScanTimeInfoAsync(requestInfo, LogDeep + 1, Token);
                    if (scanInfo == null)
                    {
                        Collector.DoProcessUiMessage(0, "could not get valid scan times info for AD connector");
                        return false;
                    }
                    if (scanInfo.NextScan == null)
                    {
                        Collector.DoProcessUiMessage(0, "not valid next scan time info for AD connector");
                        return false;
                    }

                    var nextScan = (DateTime)scanInfo.NextScan;
                    if (!(scanInfo.LastScan is DateTime lastScan) || (lastScan >= nextScan))
                    {
                        Collector.DoProcessUiMessage(0, $"currently no scan needed for AD connector, lastScan={scanInfo.LastScan} (UTC), nextScan={scanInfo.NextScan} (UTC)");
                        return false;
                    }
                }

                Collector.DoProcessUiMessage(0, "");

                var Scans = new List<cDataHistoryAdScan>();

                var LdapProps = new Dictionary<enumFasdInformationClass, Dictionary<string, cLdapPropertyInfo>>()
                {
                    {enumFasdInformationClass.User, GetPropertyList( enumFasdInformationClass.User) },
                    {enumFasdInformationClass.Computer, GetPropertyList( enumFasdInformationClass.Computer) }
                };

                using (var Conn = new cDbConnection(Collector.mainDbConnection))
                {
                    if (!Conn.IsOpen)
                    {
                        LogEntry($"Could not open main sql database '{Collector.mainDbConnection.Database}', aborting scan...'", LogLevels.Error);
                        return false;
                    }

                    // get the last as scan infos
                    var ScanHistory = await GetLastAdScansAsync(Conn, requestInfo, LogDeep + 1, Token);
                    var AlreadyScannedNodes = new Dictionary<enumFasdInformationClass, List<string>>()
                    {
                        { enumFasdInformationClass.Computer, new List<string>() },
                        { enumFasdInformationClass.User, new List<string>() }
                    };

                    // do the configured ad scans
                    var ScanCount = 0;
                    var GeneralInfo = new cDbAdGeneralInfo();
                    foreach (var Scan in Collector.InfrastructureConfig.ActiveDirectory.Scans.Values)
                    {
                        if (Scan.Type != enumFasdInformationClass.User && Scan.Type != enumFasdInformationClass.Computer)
                            continue;

                        // get the infos of the last scan
                        if (!ScanHistory.TryGetValue(Scan.Name, out var LastScanInfo))
                            LastScanInfo = null;
                        else
                        {
                            ScanHistory.Remove(Scan.Name);
                        }

                        // now we have to do the ad scan !!!
                        if (ScanCount == 0)
                            Collector.DoProcessUiMessage(0, string.Format("doing the AD scans:"));
                        ScanCount++;
                        Collector.DoProcessUiMessage(1, string.Format("starting AD scan '{0}'...", Scan.Name));

                        // create a new scan info, if not exist
                        if (LastScanInfo == null)
                        {
                            LastScanInfo = new cDbAdScanInfo()
                            {
                                ID = -1,
                                Name = Scan.Name,
                                LastRun = new DateTime(1970, 1, 1)
                            };
                            LastScanInfo.ID = await UpdateAdScanEntryAsync(Conn, LastScanInfo, requestInfo, LogDeep + 1, Token);
                        }
                        LastScanInfo.LastRun = DateTime.UtcNow;

                        // get the infos about the last node scans
                        var NodeHistory = await GetLastAdScanNode(LastScanInfo.ID, requestInfo, LogDeep + 1, Token);
                        if (NodeHistory == null)
                            NodeHistory = new Dictionary<string, cDbAdScanNode>();


                        // do the defined scan nodes
                        foreach (var ScanNode in Scan.ScanNodes)
                        {
                            // if we could not find a history for the scan node, we create a new one
                            var SName = $"{Scan.Name}|{ScanNode.AdDomain.Name}|{ScanNode.Path}";
                            if (!NodeHistory.TryGetValue(SName, out var NodeInfo))
                                NodeInfo = new cDbAdScanNode()
                                {
                                    ScanID = LastScanInfo.ID,
                                    Name = SName,
                                    Fingerprint = "",
                                    InvocationID = "0",
                                    LastUSN = 0
                                };

                            if (LdapProps != null)
                                await ProcessScanNode(Conn, LdapProps[Scan.Type], Scan, ScanNode, AlreadyScannedNodes[Scan.Type], NodeInfo, GeneralInfo, Rescan, requestInfo, LogDeep + 1, Token);
                        }

                        // and save the new scan info
                        LastScanInfo.ID = await UpdateAdScanEntryAsync(Conn, LastScanInfo, requestInfo, LogDeep + 1, Token);
                        if (LastScanInfo.ID < 0)
                        {
                            cLogManager.DefaultLogger.LogEntry(LogLevels.Error, string.Format("The ad scan db entry with ID='{0}' and Name='{1}' could not be created or updated!", LastScanInfo.ID, LastScanInfo.Name));
                            continue;
                        }

                    }
                    if (ScanCount == 0)
                        Collector.DoProcessUiMessage(0, "currently no AD scans needed.");
                    else
                        Collector.DoProcessUiMessage(0, "... AD scan finished.");

                    // update the domain list
                    await UpdateAdDomains(Conn, GeneralInfo.Domains, requestInfo, LogDeep + 1, Token);

                    // delete the remaining scan history entries, they are not needed any more
                    foreach (var Scan in ScanHistory.Values)
                    {
                        try
                        {
                            await RemoveAdScanEntryAsync(Conn, Scan.ID, requestInfo, LogDeep + 1, Token);
                        }
                        catch (Exception E)
                        {
                            cLogManager.DefaultLogger.LogException(E);
                        }
                    }
                    Collector.DoProcessUiMessage(0, "");

                }

                if (Collector.InfrastructureConfig.AzureTenants != null)
                    await DoAzureScanAsync(requestInfo, LogDeep + 1, Token);

                if (Collector.InfrastructureConfig.AzureTenants?.Values.Any(t => t.ScanIntuneDevices) == true)
                    await DoScanIntuneAsync(requestInfo, LogDeep + 1, Token);

                await Collector.SetLastScanTime("AdScan-all", 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 false;
        }

        private async Task UpdateAdDomains(cDbConnection Conn, Dictionary<string, string> Domains, 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 (!DataHistorySqlHelper.GetWellKnownSqlStatement("UpdateAdScanDomains", out var DbQuery))
                    return;

                foreach (var Entry in Domains)
                {
                    var Params = new Dictionary<string, object>(2) { { "domainDns", Entry.Key }, { "domainNB", Entry.Value } };
                    await DataHistorySqlHelper.ExecuteAsync(Conn, DbQuery, Params, requestInfo, 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);
            }
        }

        public async Task<string> GetDomainDns(string domain, 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
            {
                await UpdateDomainList(requestInfo, LogDeep + 1, Token);
                if (DomainsRev.TryGetValue(domain, out var retVal))
                    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;
        }

        public async Task<string> GetDomainNB(string domain, 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
            {
                await UpdateDomainList(requestInfo, LogDeep + 1, Token);
                if (Domains.TryGetValue(domain, out var retVal))
                    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;
        }

        public async Task UpdateDomainList(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
            {
                // check, if we have to update the cached domain list
                var DoUpdate = false;
                Dictionary<string, string> _domains = null;

                try
                {
                    var TS = DateTime.UtcNow;
                    lock (Domains)
                    {
                        if (TS > LastDomainsUpdate.AddMinutes(10) && !DomainsUpdateRunning)
                        {
                            DoUpdate = true;
                            DomainsUpdateRunning = true;
                        }
                    }

                    try
                    {
                        if (DoUpdate)
                        {
                            // read the new domain list from SQL table
                            if (DataHistorySqlHelper.GetWellKnownSqlStatement("GetAdScanDomains", out var DbQuery))
                            {
                                int apiError = 0;
                                try
                                {
                                    using (var Conn = new cDbConnection(Collector.mainDbConnection))
                                    {
                                        using (var _reader = await DataHistorySqlHelper.GetDataReaderAsync(Conn, DbQuery, null, Token))
                                        {
                                            if (_reader != null)
                                                try
                                                {
                                                    _domains = new Dictionary<string, string>();
                                                    if (_reader.HasRows)
                                                    {
                                                        while (await _reader.ReadAsync())
                                                        {
                                                            try
                                                            {
                                                                var domainDns = _reader.GetString(0);
                                                                var domainNB = _reader.GetString(1);
                                                                if (!string.IsNullOrEmpty(domainDns) && !string.IsNullOrEmpty(domainNB))
                                                                    _domains[domainDns] = domainNB;
                                                            }
                                                            catch (Exception E)
                                                            {
                                                                cLogManager.DefaultLogger.LogException(E);
                                                            }
                                                        }
                                                    }
                                                }
                                                catch (Exception E)
                                                {
                                                    cLogManager.DefaultLogger.LogException(E);
                                                }

                                        }

                                    }

                                }
                                catch (Exception E)
                                {
                                    apiError = E.HResult;
                                    LogException(E);
                                }
                                finally
                                {
                                    if (DataHistorySqlHelper.LogSql) DataHistorySqlHelper.SaveSqlTimingEntry(DbQuery.Name, _startTime, "", apiError, requestInfo?.requestName);
                                }

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


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

                if (DoUpdate)
                    // yes, we have an newer one
                    lock (Domains)
                    {
                        DomainsUpdateRunning = true;

                        if (_domains != null && _domains.Count > 0)
                        {
                            Domains.Clear();
                            DomainsRev.Clear();
                            foreach (var Entry in _domains)
                            {
                                Domains[Entry.Key] = Entry.Value;
                                DomainsRev[Entry.Value] = Entry.Key;
                            }
                        }

                        LastDomainsUpdate = DateTime.UtcNow;
                    }
            }
            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);
            }



        }

        private static void AddToLdapPropertyList(List<cLdapPropertyInfo> list, ref Dictionary<string, cLdapPropertyInfo> dicLdap)
        {
            foreach (var Entry in list)
            {
                if (!dicLdap.ContainsKey(Entry.Name))
                    dicLdap.Add(Entry.Name.ToLowerInvariant(), Entry);
            }
        }

        private static void AddToLdapPropertyList(cDataHistoryConfigColumn column, ref Dictionary<string, cLdapPropertyInfo> dicLdap)
        {
            if (column.SourceType == eDataHistoryQueryType.Static)
                return;

            if (dicLdap.TryGetValue(column.SourceName.ToLowerInvariant(), out var _entry))
                _entry.Cardinal = Math.Max(_entry.Cardinal, column.Cardinal);
            else
            {
                var ldapType = enumLdapPropertyType.ldapString;
                var prop = new cLdapPropertyInfo
                {
                    Name = column.SourceName,
                    LdapType = ldapType,
                    Cardinal = column.Cardinal
                };
                dicLdap.Add(prop.Name.ToLowerInvariant(), prop);
            }
        }

        private static void AddToLdapPropertyList(cDataHistoryConfigTable table, ref Dictionary<string, cLdapPropertyInfo> dicLdap)
        {
            foreach (var col in table.Columns.Values)
                if (col is cDataHistoryConfigColumn col2)
                    AddToLdapPropertyList(col2, ref dicLdap);
        }

        private Dictionary<string, cLdapPropertyInfo> GetPropertyList(enumFasdInformationClass InformationClass)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }

            var dicLdap = new Dictionary<string, cLdapPropertyInfo>();

            try
            {
                switch (InformationClass)
                {
                    case enumFasdInformationClass.User:
                        AddToLdapPropertyList(UserMainProperties, ref dicLdap);
                        if (Collector.InfrastructureConfig.ActiveDirectory.ScanPhoneNumbers)
                            AddToLdapPropertyList(UserPhoneProperties, ref dicLdap);
                        break;
                    case enumFasdInformationClass.Computer:
                        AddToLdapPropertyList(ComputerMainProperties, ref dicLdap);
                        break;
                }

                foreach (var DataCluster in Collector.ClusterConfig.Clusters.Values)
                {
                    if (DataCluster.Origin != enumDataHistoryOrigin.ActiveDirectory || DataCluster.InformationClass != InformationClass)
                        continue;

                    foreach (var Table in DataCluster.Tables.Values)
                        if (Table.Type == eDataHistoryTableType.Static || Table.Type == eDataHistoryTableType.History)
                            AddToLdapPropertyList(Table, ref dicLdap);
                }

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

            return dicLdap;

        }

        public override bool CheckIfLateDelivery(cDataHistoryConfigTable Table)
        {
            if (Table?.ParentCluster?.Origin == enumDataHistoryOrigin.Intune)
                return true;
            return false;
        }

        private object ConvertToF4sdType(object objVal, enumFasdValueType ValueType)
        {
            try
            {
                if (objVal != null)
                {
                    switch (ValueType)
                    {
                        case enumFasdValueType.INT:
                            return cF4SDHealthCardRawData.GetInteger(objVal);
                        case enumFasdValueType.GUID:
                            return XML.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 static string GetHash(string s)
        {
            var arr = Encoding.UTF8.GetBytes(s);
            byte[] hashBytes;
            using (System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create())
            {
                hashBytes = md5.ComputeHash(arr);
            }
            return Convert.ToBase64String(hashBytes);
        }

        private static string GetBaseDN(cDataHistoryAdScanNode Node)
        {
            try
            {
                var strDN = Node.Path;
                var arrDom = Node.AdDomain.FQDN.Split('.');
                foreach (var Entry in arrDom)
                {
                    strDN += $",DC={Entry}";
                }
                if (strDN.StartsWith(","))
                    strDN = strDN.Remove(0, 1);

                return strDN;
            }
            catch (Exception E)
            {
                LogException(E);
            }

            return null;
        }

        public Dictionary<string, string> GetNT4DomainNames()
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }

            var RetVal = new Dictionary<string, string>();
            try
            {
                foreach (var Domain in Collector?.InfrastructureConfig?.ActiveDirectory?.Domains?.Values)
                {
                    var NT4 = GetNT4DomainName(Domain);
                    if (!string.IsNullOrEmpty(NT4))
                        RetVal.Add(NT4, Domain.FQDN);
                }
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (CM != null) LogMethodEnd(CM);
            }
            return RetVal;
        }

        private static string GetNT4DomainName(cDataHistoryAdDomain Domain)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            try
            {
                IADsNameTranslate oNameTranslate = new ActiveDs.NameTranslate();
                oNameTranslate.InitEx((int)ADS_NAME_INITTYPE_ENUM.ADS_NAME_INITTYPE_DOMAIN, Domain.FQDN, Domain.Credential.User, Domain.Credential.Domain, Domain.Credential.NwCredential.Password);

                string[] aSplitDN = Domain.FQDN.Split(new Char[] { '.' });
                string sDistinguishedName = "";

                //Convert Domain Name to Distinguished Name
                foreach (string sDomainPart in aSplitDN)
                {
                    if (sDistinguishedName != "")
                        sDistinguishedName += ",";
                    sDistinguishedName = sDistinguishedName + "DC=" + sDomainPart;
                }

                oNameTranslate.Set((int)ADS_NAME_TYPE_ENUM.ADS_NAME_TYPE_UNKNOWN, sDistinguishedName);
                string sFriendlyName = oNameTranslate.Get((int)ADS_NAME_TYPE_ENUM.ADS_NAME_TYPE_NT4);
                sFriendlyName = sFriendlyName.Replace(@"\", "");
                return sFriendlyName;
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (CM != null) LogMethodEnd(CM);
            }

            return null;
        }

        public override async Task<List<object>> GetIds(cF4sdConnectorIds IdEntry, enumFasdInformationClass InfoClass, cF4sdWebRequestInfo requestInfo, int LogDeep)
        {
            await Task.CompletedTask;
            if (IdEntry.sid == null)
                return null;
            return new List<object>() { IdEntry.sid };
        }

        public override async Task<cScanTimeInfo> GetScanTimeInfoAsync(cF4sdWebRequestInfo requestInfo, int LogDeep, CancellationToken Token)
        {
            return await GetScanTimeInfoAsync(Collector.InfrastructureConfig.ActiveDirectory.ScanTiming, "AdScan-all", requestInfo, LogDeep, Token);
        }

        public override async Task<bool> ColumnUpatePostProcessingAsync(cDbConnection DbConn, 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 (!DataHistorySqlHelper.GetWellKnownSqlStatement("PostColumnCreateActiveDirectory", out var Query2))
                    return false;

                await DataHistorySqlHelper.ExecuteAsync(DbConn, Query2, null, requestInfo, 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 false;
        }

        public override async Task<bool> ValidateMainTablesAsync(cDbConnection DbConn, 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
            {
                // validate the ad scan tables
                Collector.DoProcessUiMessage(1, $"start validating the ad scan tables...");
                if (!DataHistorySqlHelper.GetWellKnownSqlStatement("Validate_AdScanTables", out var Query))
                    return false;
                var n = await DataHistorySqlHelper.GetScalarResultAsync<int>(DbConn, Query, null, requestInfo, Token);
                if (n > 0)
                {
                    Collector.DoProcessUiMessage(1, $"the validation of the ad scan tables finished successful.");
                    return true;
                }


                // create the ad scan tables
                Collector.DoProcessUiMessage(2, $"the ad scan tables are not complete, creating them...");
                if (!DataHistorySqlHelper.GetWellKnownSqlStatement("Create_AdScanTables", out Query))
                    return false;
                var n2 = await DataHistorySqlHelper.ExecuteAsync(DbConn, Query, null, requestInfo, Token);

                // validate the created ad scan tables
                if (!DataHistorySqlHelper.GetWellKnownSqlStatement("Validate_AdScanTables", out Query))
                    return false;
                n = await DataHistorySqlHelper.GetScalarResultAsync<int>(DbConn, Query, null, requestInfo, Token);
                if (n > 0)
                {
                    Collector.DoProcessUiMessage(2, $"the ad scan tables were created sucessfull.");
                    return true;
                }

                Collector.DoProcessUiMessage(2, "An error occurred when creating the ad main tables.");
            }
            catch (Exception E)
            {
                Collector.DoProcessUiMessage(1, $"the validation of the ad scan tables aborted with an exceptional error.");
                LogException(E);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }

            LogEntry($"the main tables could not be validated sucessfull.", LogLevels.Fatal);

            return false;
        }

        public void ResolveWinAuthenticationGroups()
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            try
            {
                if (Collector?.InfrastructureConfig?.Authorisation?.MembershipGroups?.GroupsAd is null)
                    return;

                LogEntry("Resolving AD groups SIDs...");
                var domains = Collector.InfrastructureConfig.Authorisation.MembershipGroups.GroupsAd.Values;
                LogEntry("Resolving AD groups SIDs finished");

                bool hasResolved = true;

                foreach (cF4SDMembershipGroupsAd domain in domains)
                {
                    hasResolved = hasResolved && TryProcessDomain(domain);
                }

                WinAuthenticationGropsResolved = hasResolved;
            }
            catch (Exception ex)
            {
                LogException(ex);
                WinAuthenticationGropsResolved = false;
            }
            finally
            {
                if (CM != null) LogMethodEnd(CM);
            }
        }

        private bool TryProcessDomain(cF4SDMembershipGroupsAd domain)
        {
            try
            {
                bool hasProcessed = true;
                if (domain?.Domain is null)
                    return hasProcessed;

                LogEntry($"Start processing domain");
                string domainName = domain.Domain.FQDN;
                LogEntry($"Processing domain {domainName}");

                PrincipalContext principalContext;
                if (domain.Domain.Credential == null)
                    principalContext = new PrincipalContext(ContextType.Domain, domainName);
                else
                    principalContext = new PrincipalContext(ContextType.Domain, domainName, domain.Domain.Credential.User, domain.Domain.Credential.NwCredential.Password);

                LogEntry($"Principal context successful created");

                foreach (var group in domain.Values)
                {
                    hasProcessed = hasProcessed && TryProcessMembershipGroup(group, principalContext, domainName);
                }

                return hasProcessed;
            }
            catch (Exception ex)
            {
                LogException(ex);
                return false;
            }
            finally
            {
                LogEntry($"End processing domain");
            }
        }

        private bool TryProcessMembershipGroup(cF4SDMembershipGroupAd group, PrincipalContext principalContext, string domainName)
        {
            try
            {
                if (group is null)
                    return true;

                LogEntry($"Start processing group.");
                LogEntry($"Group: {group.Name}.");
                var groupIdentity = GroupPrincipal.FindByIdentity(principalContext, IdentityType.SamAccountName, group.Account);
                LogEntry($"Group principal found");
                LogEntry($"GI.SID: {groupIdentity.Sid}");

                if (groupIdentity?.Sid != null)
                {
                    group.SID = groupIdentity.Sid.ToString();
                    LogEntry($"AD group '{group.Account}' in domain '{domainName}' resolved. SID = {group.SID}");
                }

                if (group.SID == null)
                    LogEntry($"Could not resolve AD group '{group.Account}' in domain '{domainName}'", LogLevels.Warning);

                return true;
            }
            catch (Exception ex)
            {
                LogException(ex);
                return false;
            }
            finally
            {
                LogEntry($"End processing group.");
            }
        }


        static public Guid ConvertAzureSidToGuid(string sid)
        {
            if (!sid.StartsWith("S-1-12-1-"))
                return Guid.Empty;

            try
            {
                var objSid = GetByteArrayFromSid(sid);
                if (objSid == null || objSid.Length != 28)
                    return Guid.Empty;

                var arrB = new byte[16];
                Array.Copy(objSid, 12, arrB, 0, 16);
                var azureId = new Guid(arrB);

                return azureId;
            }
            catch (Exception)
            {
                return Guid.Empty;
            }
        }
    }


    public class cDbAdScanInfo
    {
        public int ID;
        public string Name;
        public DateTime LastRun;
    }

    public class cDbAdScanNode
    {
        public int ScanID;
        public string Name;
        public string Fingerprint;
        public string InvocationID;
        public UInt64 LastUSN;
        public string domainDNS;
        public string domainNB;
    }

    public class cDbAdGeneralInfo
    {
        public Dictionary<string, string> Domains = new Dictionary<string, string>();
    }

    public class cLdapPropertyInfo
    {
        public string Name { get; set; }
        public enumLdapPropertyType LdapType { get; set; }
        public string DbName { get; set; }

        public bool Cumulative { get; set; } = false;
        public bool Reverse { get; set; } = false;

        public int Cardinal = -1;

        public cLdapPropertyInfo()
        {

        }
    }
}


