using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;

namespace Disco.Services.Interop.ActiveDirectory
{
    public class ActiveDirectoryGroupCache
    {
        private ConcurrentDictionary<SecurityIdentifier, Tuple<ADGroup, DateTime>> securityIdentifierCache;
        private ConcurrentDictionary<string, Tuple<ADGroup, DateTime>> distinguishedNameCache;
        private const long CacheTimeoutTicks = 6000000000; // 10 Minutes
        
        private const int CacheCleanIntervalMinutes = 15;
        private DateTime cacheCleanNext;
        private object cacheCleanLock = new object();
        private Task cacheCleanTask;

        public ActiveDirectoryGroupCache()
        {
            this.securityIdentifierCache = new ConcurrentDictionary<SecurityIdentifier, Tuple<ADGroup, DateTime>>();
            this.distinguishedNameCache = new ConcurrentDictionary<string, Tuple<ADGroup, DateTime>>(StringComparer.OrdinalIgnoreCase);
            cacheCleanNext = DateTime.Now.AddMinutes(CacheCleanIntervalMinutes);
        }

        public ADGroup GetGroup(string DistinguishedName)
        {
            // Check Cache
            Tuple<ADGroup, DateTime> groupRecord = TryDistinguishedNameCache(DistinguishedName);

            if (groupRecord == null)
            {
                // Load from AD
                var group = ActiveDirectory.RetrieveADGroupByDistinguishedName(DistinguishedName);
                SetValue(group);

                return group;
            }
            else
            {
                // Return from Cache
                return groupRecord.Item1;
            }
        }
        public ADGroup GetGroup(SecurityIdentifier SecurityIdentifier)
        {
            // Check Cache
            Tuple<ADGroup, DateTime> groupRecord = TrySecurityIdentifierCache(SecurityIdentifier);

            if (groupRecord == null)
            {
                // Load from AD
                var group = ActiveDirectory.RetrieveADGroupWithSecurityIdentifier(SecurityIdentifier);
                SetValue(group);

                return group;
            }
            else
            {
                // Return from Cache
                return groupRecord.Item1;
            }
        }

        public IEnumerable<ADGroup> GetRecursiveGroups(IEnumerable<string> DistinguishedNames)
        {
            List<ADGroup> groups = new List<ADGroup>();

            foreach (var distinguishedName in DistinguishedNames)
                foreach (var group in GetGroupsRecursive(distinguishedName, new Stack<ADGroup>()))
                    if (!groups.Contains(group))
                    {
                        groups.Add(group);
                        yield return group;
                    }
        }
        public IEnumerable<ADGroup> GetRecursiveGroups(string DistinguishedName)
        {
            foreach (var group in GetGroupsRecursive(DistinguishedName, new Stack<ADGroup>()))
                yield return group;
        }
        private IEnumerable<ADGroup> GetGroupsRecursive(string DistinguishedName, Stack<ADGroup> RecursiveTree)
        {
            var group = GetGroup(DistinguishedName);

            if (group != null && !RecursiveTree.Contains(group))
            {
                yield return group;

                if (group.MemberOf != null)
                {
                    RecursiveTree.Push(group);

                    foreach (var parentDistinguishedName in group.MemberOf)
                        foreach (var parentGroup in GetGroupsRecursive(parentDistinguishedName, RecursiveTree))
                            yield return parentGroup;

                    RecursiveTree.Pop();
                }
            }
        }

        private Tuple<ADGroup, DateTime> TryDistinguishedNameCache(string DistinguishedName)
        {
            Tuple<ADGroup, DateTime> groupRecord;
            if (distinguishedNameCache.TryGetValue(DistinguishedName, out groupRecord))
            {
                if (groupRecord.Item2 > DateTime.Now)
                    return groupRecord;
                else
                {
                    if (distinguishedNameCache.TryRemove(DistinguishedName, out groupRecord))
                        securityIdentifierCache.TryRemove(groupRecord.Item1.SecurityIdentifier, out groupRecord);
                }
            }
            return null;
        }
        private Tuple<ADGroup, DateTime> TrySecurityIdentifierCache(SecurityIdentifier SecurityIdentifier)
        {
            Tuple<ADGroup, DateTime> groupRecord;
            if (securityIdentifierCache.TryGetValue(SecurityIdentifier, out groupRecord))
            {
                if (groupRecord.Item2 > DateTime.Now)
                    return groupRecord;
                else
                {
                    if (securityIdentifierCache.TryRemove(SecurityIdentifier, out groupRecord))
                        distinguishedNameCache.TryRemove(groupRecord.Item1.DistinguishedName, out groupRecord);
                }
            }
            return null;
        }

        private bool SetValue(ADGroup Group)
        {
            Tuple<ADGroup, DateTime> groupRecord = Tuple.Create(Group, DateTime.Now.AddTicks(CacheTimeoutTicks));
            Tuple<ADGroup, DateTime> oldGroupRecord;

            var distinguishedName = Group.DistinguishedName;
            var securityIdentifier = Group.SecurityIdentifier;

            if (distinguishedNameCache.ContainsKey(distinguishedName))
            {
                if (distinguishedNameCache.TryGetValue(distinguishedName, out oldGroupRecord))
                {
                    distinguishedNameCache.TryUpdate(distinguishedName, groupRecord, oldGroupRecord);
                }
            }
            else
            {
                distinguishedNameCache.TryAdd(distinguishedName, groupRecord);
            }

            if (securityIdentifierCache.ContainsKey(securityIdentifier))
            {
                if (securityIdentifierCache.TryGetValue(securityIdentifier, out oldGroupRecord))
                {
                    securityIdentifierCache.TryUpdate(securityIdentifier, groupRecord, oldGroupRecord);
                }
            }
            else
            {
                securityIdentifierCache.TryAdd(securityIdentifier, groupRecord);
            }
            return true;
        }

        #region Stale Cache Clean

        private void EnsureCleanCache()
        {
            if (cacheCleanTask == null && cacheCleanNext < DateTime.Now)
            {
                lock (cacheCleanLock)
                {
                    if (cacheCleanTask == null && cacheCleanNext < DateTime.Now)
                    {
                        cacheCleanTask = Task.Factory.StartNew(CleanCache);
                    }
                }
            }
        }

        private void CleanCache()
        {
            DateTime now = DateTime.Now;

            // Clean Cache
            var dnKeys = distinguishedNameCache.Keys.ToArray();
            foreach (var dnKey in dnKeys)
            {
                Tuple<ADGroup, DateTime> groupRecord;
                if (distinguishedNameCache.TryGetValue(dnKey, out groupRecord))
                {
                    if (groupRecord.Item2 <= now)
                    {
                        distinguishedNameCache.TryRemove(dnKey, out groupRecord);
                    }
                }
            }

            // Clean SID Cache
            var siKeys = securityIdentifierCache.Keys.ToArray();
            foreach (var siKey in siKeys)
            {
                Tuple<ADGroup, DateTime> groupRecord;
                if (securityIdentifierCache.TryGetValue(siKey, out groupRecord))
                {
                    if (groupRecord.Item2 <= now)
                    {
                        securityIdentifierCache.TryRemove(siKey, out groupRecord);
                    }
                }
            }

            // Schedule Next Clean
            cacheCleanNext = now.AddMinutes(CacheCleanIntervalMinutes);
            cacheCleanTask = null;
        }

        #endregion
    }
}
