
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.DirectoryServices;
using System.DirectoryServices.ActiveDirectory;
using System.DirectoryServices.AccountManagement;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using C4IT.Logging;
using static C4IT.Logging.cLogManager;

namespace LiamAD
{
    public class cActiveDirectoryBase
    {
        private cADLogonInfo privLogonInfo = null;
        public PrincipalContext adContext = null;
        public DirectoryEntry directoryEntry = null;
        public string EffectiveDomainController { get; private set; } = null;
        public Exception LastException { get; private set; } = null;
        public string LastErrorMessage { get; private set; } = null;

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void ResetError()
        {
            LastException = null;
            LastErrorMessage = null;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void SetErrorException(string Action, Exception E, LogLevels lev = LogLevels.Error)
        {
            LastException = E;
            LastErrorMessage = Action + ": " + E.Message;
            cLogManager.LogEntry(Action, lev);
        }

        private async Task<bool> privLogonAsync(cADLogonInfo LogonInfo)
        {
            try
            {
                //TODO: remove dummy delay?
                await Task.Delay(0);
                ResetError();
                var adServer = ResolveEffectiveDomainController(LogonInfo);
                adContext = new PrincipalContext(ContextType.Domain, adServer, LogonInfo.User, new NetworkCredential("", LogonInfo.UserSecret).Password);
                EffectiveDomainController = adContext.ConnectedServer ?? adServer;
                LogEntry($"AD provider domain controller pinned to '{EffectiveDomainController}' for domain '{LogonInfo.Domain}'.", LogLevels.Debug);

                var ldapPath = $"LDAP://{EffectiveDomainController}/{LogonInfo.TargetGroupPath}";
                directoryEntry = new DirectoryEntry
                {
                    Path = ldapPath,
                    Username = LogonInfo.User,
                    Password = new NetworkCredential(LogonInfo.User, LogonInfo.UserSecret).Password,
                    AuthenticationType = AuthenticationTypes.Secure | AuthenticationTypes.Sealing
                };
                return adContext != null;
            }
            catch (Exception E)
            {
                SetErrorException("exception error while ad login", E, LogLevels.Debug);
                cLogManager.LogException(E, LogLevels.Debug);
            }

            return false;
        }

        private string ResolveEffectiveDomainController(cADLogonInfo logonInfo)
        {
            var configuredDomainControllers = ParseDomainControllers(logonInfo?.DomainControllers);
            foreach (var domainController in configuredDomainControllers)
            {
                if (CanConnectToDomainController(domainController, logonInfo))
                    return domainController;

                LogEntry($"Configured AD provider domain controller '{domainController}' is not reachable. Trying next candidate.", LogLevels.Warning);
            }

            var pdc = TryGetPdcRoleOwner(logonInfo);
            if (!string.IsNullOrWhiteSpace(pdc))
                return pdc;

            LogEntry($"Could not determine PDC emulator for domain '{logonInfo?.Domain}'. Falling back to domain locator.", LogLevels.Warning);
            return logonInfo?.Domain;
        }

        private static IEnumerable<string> ParseDomainControllers(string domainControllers)
        {
            if (string.IsNullOrWhiteSpace(domainControllers))
                return Enumerable.Empty<string>();

            return domainControllers
                .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
                .Select(i => i.Trim())
                .Where(i => !string.IsNullOrWhiteSpace(i));
        }

        private bool CanConnectToDomainController(string domainController, cADLogonInfo logonInfo)
        {
            try
            {
                using (var context = new PrincipalContext(ContextType.Domain, domainController, logonInfo.User, new NetworkCredential("", logonInfo.UserSecret).Password))
                    return !string.IsNullOrWhiteSpace(context.ConnectedServer);
            }
            catch (Exception E)
            {
                LogException(E, LogLevels.Debug);
                return false;
            }
        }

        private string TryGetPdcRoleOwner(cADLogonInfo logonInfo)
        {
            try
            {
                var credentials = new DirectoryContext(
                    DirectoryContextType.Domain,
                    logonInfo.Domain,
                    logonInfo.User,
                    new NetworkCredential("", logonInfo.UserSecret).Password);

                using (var domain = Domain.GetDomain(credentials))
                    return domain?.PdcRoleOwner?.Name;
            }
            catch (Exception E)
            {
                LogException(E, LogLevels.Debug);
                return null;
            }
        }

        private string GetAdServer()
        {
            if (!string.IsNullOrWhiteSpace(EffectiveDomainController))
                return EffectiveDomainController;

            return privLogonInfo?.Domain;
        }

        private async Task<bool> privRelogon()
        {
            if (privLogonInfo == null)
                return false;
            var RetVal = await privLogonAsync(privLogonInfo);
            return RetVal;
        }

        public async Task<bool> LogonAsync(cADLogonInfo LogonInfo)
        {
            var RetVal = await privLogonAsync(LogonInfo);
            if (RetVal == true)
                privLogonInfo = LogonInfo;
            return RetVal;
        }

        internal AuthorizationRuleCollection GetAccessControlList(string path)
        {
            var CM = MethodBase.GetCurrentMethod();
            LogMethodBegin(CM);
            try
            {
                DirectoryInfo dADir = new DirectoryInfo(path);
                var dAACL = dADir.GetAccessControl();
                return dAACL.GetAccessRules(true, false, typeof(System.Security.Principal.SecurityIdentifier));
            }
            catch (Exception E)
            {
                LogException(E);
                return null;
            }
            finally
            {
                LogMethodEnd(CM);
            }

        }

        internal string resolveSid(string sid)
        {

            try
            {
                return new System.Security.Principal.SecurityIdentifier(sid).Translate(typeof(System.Security.Principal.NTAccount)).ToString();
            }
            catch (Exception E)
            {
                LogException(E);
                return null;
            }
        }

        internal async Task<cADCollectionBase> RequestSecurityGroupsListAsync(string groupFilter)
        {
            var CM = MethodBase.GetCurrentMethod();
            LogMethodBegin(CM);
            try
            {
                await Task.Delay(0);

                var Result = privRequestSecurityGroupsListAsync(groupFilter);
                if (Result != null)
                {
                    var RetVal = new cADCollectionBase(Result.Count);
                    foreach (var Entry in Result)
                    {
                        var res = new cSecurityGroupResult(Entry);
                        RetVal.Add(res);
                    }
                    return RetVal;
                }
            }
            catch (Exception E)
            {
                LogException(E);
                return null;
            }
            finally
            {
                LogMethodEnd(CM);
            }
            return null;
        }

        private List<cSecurityGroupResult> privRequestSecurityGroupsListAsync(string groupFilter = null, string rawLDAPFilter = null)
        {
            ResetError();
            List<cSecurityGroupResult> securityGroups = new List<cSecurityGroupResult>();

            if (String.IsNullOrEmpty(privLogonInfo.TargetGroupPath) ||
                (string.IsNullOrEmpty(groupFilter) && string.IsNullOrEmpty(rawLDAPFilter)))
            {
                return new List<cSecurityGroupResult>();
            }

            try
            {
                var ctx = adContext;
                var entry = directoryEntry;

                using (DirectorySearcher dSearch = new DirectorySearcher(entry))
                {
                    dSearch.Filter = string.IsNullOrEmpty(rawLDAPFilter) ? "(&(" + groupFilter + ")(objectClass=group))" : rawLDAPFilter;
                    dSearch.PageSize = 100000;

                    SearchResultCollection sr = dSearch.FindAll();
                    if (sr.Count > 0)
                    {
                        foreach (SearchResult k in sr)
                        {
                            var sid = new SecurityIdentifier(k.Properties["objectSid"][0] as byte[], 0).Value;
                            var dn = k.Properties["distinguishedname"][0].ToString();

                            // Initialisieren Sie die managedBy-SID als null
                            string managedBySid = null;

                            // Prüfen, ob das managedBy-Attribut existiert und nicht null ist
                            if (k.Properties.Contains("managedBy") && k.Properties["managedBy"].Count > 0)
                            {
                                // managedBy-DN erhalten
                                string managedByDn = k.Properties["managedBy"][0].ToString();

                                // Erstellen eines DirectoryEntry-Objekts für den managedBy-DN
                                using (DirectoryEntry managedByEntry = new DirectoryEntry($"LDAP://{GetAdServer()}/{managedByDn}", privLogonInfo.User, new NetworkCredential("", privLogonInfo.UserSecret).Password, AuthenticationTypes.Secure | AuthenticationTypes.Sealing))
                                {
                                    if (managedByEntry.Properties.Contains("objectSid") && managedByEntry.Properties["objectSid"].Count > 0)
                                    {
                                        // SID des managedBy-Objekts erhalten
                                        managedBySid = new SecurityIdentifier(managedByEntry.Properties["objectSid"][0] as byte[], 0).Value;
                                    }
                                }
                            }

                            // Neues SecurityGroup-Objekt erstellen
                            cSecurityGroupResult group = new cSecurityGroupResult()
                            {
                                ID = sid,
                                Path = dn,
                                DisplayName = k.Properties["Name"][0].ToString(),
                                Description = k.Properties.Contains("Description") ? k.Properties["Description"][0].ToString() : string.Empty,
                                Scope = (GroupScope)GroupPrincipal.FindByIdentity(ctx, IdentityType.Sid, sid).GroupScope,
                                ManagedBySID = managedBySid
                            };

                            securityGroups.Add(group);
                        }
                    }
                }
            }
            catch (Exception e)
            {
                cLogManager.LogException(e);
                return new List<cSecurityGroupResult>(securityGroups);
            }

            return securityGroups;
        }

        public class cADCollectionBase : SortedList<string, cADResultBase>
        {
            public cADCollectionBase() { }
            public cADCollectionBase(int n) : base(n) { }

            public void Add(cADResultBase adr)
            {
                if (!this.ContainsKey(adr.ID))
                    this.Add(adr.ID, adr);
            }
        }

        public class cSecurityGroupResult : cADResultBase
        {
            public cSecurityGroupResult() { }
            public cSecurityGroupResult(cADResultBase b) : base(b)
            {
                this.ManagedBySID = (b as cSecurityGroupResult)?.ManagedBySID;
            }
            public GroupScope Scope { get; internal set; }
            public string ManagedBySID { get; internal set; }
        }
        public class cADUserResult : cADResultBase
        {
            public string GivenName { get; internal set; }
            public string SurName { get; internal set; }
            public string UserPrincipalName { get; internal set; }
            public string Email { get; internal set; }
            public cADUserResult() { }
            public cADUserResult(cADResultBase b) : base(b)
            {

            }

            public cADUserResult(Principal Result) : base(Result)
            {
                UserPrincipalName = Result.UserPrincipalName;
            }

            public GroupScope Scope { get; internal set; }
        }
        public class cADResultBase
        {
            public string ID { get; set; } = null;
            public string DisplayName { get; set; } = null;
            public string Path { get; set; } = null;
            public DateTime CreatedDate { get; set; } = DateTime.MinValue;
            public string Description { get; set; } = null;

            public cADResultBase()
            { }
            public cADResultBase(cADResultBase Result)
            {
                if (Result == null)
                    return;

                ID = Result.ID;
                DisplayName = Result.DisplayName;
                Description = Result.Description;
                Path = Result.Path;
            }
            public cADResultBase(Principal Result)
            {
                if (Result == null)
                    return;

                ID = Result.Sid.ToString();
                DisplayName = Result.DisplayName;
                Path = Result.DistinguishedName;
            }
        }

        /// <summary>
        /// Asynchrones Abrufen des managedBy-Attributs einer AD-Gruppe anhand ihrer SID.
        /// Gibt den Distinguished Name (DN) des Managers zurück.
        /// </summary>
        /// <param name="groupSid">Die SID der AD-Gruppe.</param>
        /// <returns>Der DN des Managers oder null, falls nicht gefunden.</returns>
        public async Task<string> GetManagedByDnAsync(string dn)
        {
            try
            {
                // Simuliert einen asynchronen Aufruf
                await Task.Yield();

                using (var group = GroupPrincipal.FindByIdentity(adContext, IdentityType.DistinguishedName, dn))
                {
                    if (group == null)
                    {
                        cLogManager.LogEntry($"Gruppe mit dn {dn} nicht gefunden.");
                        return null;
                    }

                    // Zugriff auf das underlying DirectoryEntry, um das managedBy-Attribut zu lesen
                    var directoryEntry = group.GetUnderlyingObject() as DirectoryEntry;
                    if (directoryEntry != null && directoryEntry.Properties.Contains("managedBy"))
                    {
                        var managedByValue = directoryEntry.Properties["managedBy"].Value as string;
                        if (!string.IsNullOrEmpty(managedByValue))
                        {
                            return managedByValue;
                        }
                        else
                        {
                            cLogManager.LogEntry($"managedBy-Attribut für Gruppe mit dn {dn} ist leer.");
                        }
                    }
                    else
                    {
                        cLogManager.LogEntry($"Gruppe mit dn {dn} hat kein managedBy-Attribut.");
                    }
                }
            }
            catch (Exception ex)
            {
                cLogManager.LogException(ex);
            }

            return null;
        }

        /// <summary>
        /// Asynchrones Abrufen der Mitglieder des Managers einer AD-Gruppe anhand des groupSid.
        /// Verwendet den DN des Managers.
        /// </summary>
        /// <param name="groupSid">Die SID der AD-Gruppe.</param>
        /// <returns>Eine cADCollectionBase mit den Mitgliedern oder null.</returns>
        public async Task<cADCollectionBase> GetManagedByMembersAsync(string groupSid)
        {
            try
            {
                var managedByDn = await GetManagedByDnAsync(groupSid);
                if (!string.IsNullOrEmpty(managedByDn))
                {
                    return await GetMembersByDnAsync(managedByDn);
                }
                else
                {
                    cLogManager.LogEntry($"Keine gültige managedBy DN für Gruppe mit SID {groupSid} gefunden.");
                }
            }
            catch (Exception ex)
            {
                cLogManager.LogException(ex);
            }

            return null;
        }

        /// <summary>
        /// Asynchrones Abrufen der Mitglieder einer AD-Gruppe anhand des Distinguished Name (DN).
        /// </summary>
        /// <param name="dn">Der Distinguished Name der AD-Gruppe.</param>
        /// <returns>Eine cADCollectionBase mit den Mitgliedern oder null.</returns>
        public async Task<cADCollectionBase> GetMembersByDnAsync(string dn)
        {
            try
            {
                // Simuliert einen asynchronen Aufruf
                await Task.Yield();

                var result = privGetMembersByDnAsync(dn).ToList();
                if (result != null && result.Any())
                {
                    var retVal = new cADCollectionBase(result.Count);
                    foreach (var entry in result)
                    {
                        var res = new cADUserResult(entry);
                        if (!string.IsNullOrEmpty(res.Path))
                            retVal.Add(res);
                    }
                    return retVal;
                }
                else
                {
                    cLogManager.LogEntry($"Keine Mitglieder für Gruppe mit DN {dn} gefunden.");
                }
            }
            catch (Exception e)
            {
                cLogManager.LogException(e);
            }

            return null;
        }

        /// <summary>
        /// Interne Methode zum Abrufen der Mitglieder einer AD-Gruppe anhand des Distinguished Name (DN).
        /// </summary>
        /// <param name="dn">Der Distinguished Name der AD-Gruppe.</param>
        /// <returns>Eine PrincipalSearchResult mit den Mitgliedern oder null.</returns>
        private PrincipalSearchResult<Principal> privGetMembersByDnAsync(string dn)
        {
            try
            {
                using (var group = GroupPrincipal.FindByIdentity(adContext, IdentityType.DistinguishedName, dn))
                {
                    if (group == null)
                    {
                        cLogManager.LogEntry($"Gruppe mit DN {dn} nicht gefunden.");
                        return null;
                    }
                    else
                    {
                        return group.GetMembers(true);
                    }
                }
            }
            catch (Exception e)
            {
                cLogManager.LogException(e);
            }

            return null;
        }

        /// <summary>
        /// Asynchrones Abrufen der Mitglieder einer AD-Gruppe anhand ihrer SID.
        /// Diese Methode bleibt unverändert und kann weiterhin verwendet werden.
        /// </summary>
        /// <param name="sid">Die SID der AD-Gruppe.</param>
        /// <returns>Eine cADCollectionBase mit den Mitgliedern oder null.</returns>
        internal async Task<cADCollectionBase> GetMembersAsync(string sid)
        {
            try
            {
                // Simuliert einen asynchronen Aufruf
                await Task.Yield();

                var result = privGetMembersAsync(sid).ToList();
                if (result != null && result.Any())
                {
                    var retVal = new cADCollectionBase(result.Count);
                    foreach (var entry in result)
                    {
                        var res = new cADUserResult(entry);
                        if (!string.IsNullOrEmpty(res.Path))
                            retVal.Add(res);
                    }
                    return retVal;
                }
                else
                {
                    cLogManager.LogEntry($"Keine Mitglieder für Gruppe mit SID {sid} gefunden.");
                }
            }
            catch (Exception e)
            {
                cLogManager.LogException(e);
            }

            return null;
        }

        /// <summary>
        /// Interne Methode zum Abrufen der Mitglieder einer AD-Gruppe anhand ihrer SID.
        /// Diese Methode bleibt unverändert und kann weiterhin verwendet werden.
        /// </summary>
        /// <param name="sid">Die SID der AD-Gruppe.</param>
        /// <returns>Eine PrincipalSearchResult mit den Mitgliedern oder null.</returns>
        private PrincipalSearchResult<Principal> privGetMembersAsync(string sid)
        {
            try
            {
                using (var group = GroupPrincipal.FindByIdentity(adContext, IdentityType.Sid, sid))
                {
                    if (group == null)
                    {
                        cLogManager.LogEntry($"Gruppe mit SID {sid} nicht gefunden.");
                        return null;
                    }
                    else
                    {
                        return group.GetMembers(true);
                    }
                }
            }
            catch (Exception e)
            {
                cLogManager.LogException(e);
            }

            return null;
        }
    }
}
