
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 LiamNtfs
{
    public class cActiveDirectoryBase
    {
        private cNtfsLogonInfo 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(cNtfsLogonInfo 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($"NTFS AD 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(cNtfsLogonInfo logonInfo)
        {
            var configuredDomainControllers = ParseDomainControllers(logonInfo?.DomainControllers);
            foreach (var domainController in configuredDomainControllers)
            {
                if (CanConnectToDomainController(domainController, logonInfo))
                    return domainController;

                LogEntry($"Configured NTFS AD 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, cNtfsLogonInfo 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(cNtfsLogonInfo 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 async Task<bool> privRelogon()
        {
            if (privLogonInfo == null)
                return false;
            var RetVal = await privLogonAsync(privLogonInfo);
            return RetVal;
        }

        public async Task<bool> LogonAsync(cNtfsLogonInfo 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>();
            try
            {
                var res = new List<cSecurityGroupResult>();
                if (String.IsNullOrEmpty(privLogonInfo.TargetGroupPath) || string.IsNullOrEmpty(groupFilter) && string.IsNullOrEmpty(rawLDAPFilter))
                    return res;
                try
                {
                    var ctx = adContext;
                    var entry = directoryEntry;
                    DirectorySearcher dSearch = new DirectorySearcher(entry)
                    {
                        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();
                            cSecurityGroupResult group = new cSecurityGroupResult()
                            {
                                ID = sid,
                                Path = k.Properties["distinguishedname"][0].ToString(),
                                DisplayName = k.Properties["Name"][0].ToString(),
                                Scope = (GroupScope)GroupPrincipal.FindByIdentity(ctx, IdentityType.Sid, sid).GroupScope
                            };
                            securityGroups.Add(group);
                        }
                    }
                }
                catch
                {
                    return new List<cSecurityGroupResult>(securityGroups);
                }
                return new List<cSecurityGroupResult>(securityGroups);
            }
            catch (Exception E)
            {
                cLogManager.LogException(E);
            }

            return null;
        }
        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) { }
            public GroupScope Scope { 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 cADResultBase()
            { }
            public cADResultBase(cADResultBase Result)
            {
                if (Result == null)
                    return;

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

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

        internal async Task<cADCollectionBase> GetMembersAsync(string Sid)
        {
            try
            {
                await Task.Delay(0);
                var Result = privGetMembersAsync(Sid).ToList();
                if (Result != null)
                {
                    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;
                }
            }
            catch (Exception E)
            {
                cLogManager.LogException(E);
            }

            return null;
        }

        private PrincipalSearchResult<Principal> privGetMembersAsync(string sid)
        {
            try
            {
                using (var group = GroupPrincipal.FindByIdentity(adContext,sid))
                {
                    if (group == null)
                    {
                        return null;
                    }
                    else
                    {
                        return group.GetMembers(true);
                    }
                }
            }
            catch (Exception E)
            {
                cLogManager.LogException(E);
            }

            return null;
        }
    }
}
