using System.Diagnostics.CodeAnalysis;
using System.DirectoryServices;
using Microsoft.Extensions.Caching.Memory;
using System.DirectoryServices.AccountManagement;
using Microsoft.Extensions.Options;
using DigitalData.Core.Abstraction.Application.DTO;
using DigitalData.Core.Abstraction.Application;

namespace DigitalData.Core.Application
{
    //TODO: rename as DirectorySearcher
    [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "<Pending>")]
    public class DirectorySearchService : IDirectorySearchService
    {
        private readonly IMemoryCache _memoryCache;
        public string ServerName { get; }
        public string Root { get; }
        public string SearchRootPath { get; }
        private readonly DateTimeOffset? _userCacheExpiration;
        public Dictionary<string, string> CustomSearchFilters { get; }

        /// <summary>
        /// Initializes a new instance of the <see cref="DirectorySearchService"/> class.
        /// </summary>
        /// <param name="options">The options for directory search.</param>
        /// <param name="memoryCache">The memory cache.</param>
        /// <exception cref="InvalidOperationException">
        /// Thrown if the server name or root directory is not configured.
        /// </exception>
        public DirectorySearchService(IOptions<DirectorySearchOptions> options, IMemoryCache memoryCache)
        {
            _memoryCache = memoryCache;

            var dirSearchOptions = options.Value;

            ServerName = dirSearchOptions.ServerName;

            Root = dirSearchOptions.Root;

            SearchRootPath = $"LDAP://{ServerName}/{Root}";
            
            CustomSearchFilters = dirSearchOptions.CustomSearchFilters;

            if(dirSearchOptions.UserCacheExpirationDays is double expirationDays)
                _userCacheExpiration = DateTimeOffset.Now.Date.AddDays(expirationDays);
        }

        /// <summary>
        /// Creates the connections to the server and returns a Boolean value that specifies
        /// whether the specified username and password are valid.
        /// </summary>
        /// <param name="userName">The username that is validated on the server. See the Remarks section
        /// for more information on the format of userName.</param>
        /// <param name="password">The password that is validated on the server.</param>
        /// <returns>True if the credentials are valid; otherwise, false.</returns>
        public bool ValidateCredentials(string userName, string password)
        {
            using var context = new PrincipalContext(ContextType.Domain, ServerName, Root);
            return context.ValidateCredentials(userName, password);
        }

        /// <summary>
        /// Creates the connections to the server asynchronously and returns a Boolean value that specifies
        /// whether the specified username and password are valid.
        /// </summary>
        /// <param name="userName">The username that is validated on the server. See the Remarks section
        /// for more information on the format of userName.</param>
        /// <param name="password">The password that is validated on the server.</param>
        /// <returns>True if the credentials are valid; otherwise, false.</returns>
        public Task<bool> ValidateCredentialsAsync(string userName, string password) => Task.Run(()
            => ValidateCredentials(userName, password));

        //TODO: remove unnecessary DataResult
        /// <summary>
        /// Finds all directory entries matching the specified filter.
        /// </summary>
        /// <param name="searchRoot">The search root.</param>
        /// <param name="filter">The search filter.</param>
        /// <param name="searchScope">The search scope.</param>
        /// <param name="sizeLimit">The size limit.</param>
        /// <param name="properties">The properties to load.</param>
        /// <returns>A <see cref="DataResult{T}"/> containing the results.</returns>
        public DataResult<IEnumerable<ResultPropertyCollection>> FindAll(DirectoryEntry searchRoot, string filter, SearchScope searchScope = SearchScope.Subtree, int sizeLimit = 5000, params string[] properties)
        {
            List<ResultPropertyCollection> list = new();

            using var searcher = new DirectorySearcher()
            {
                Filter = filter,
                SearchScope = searchScope,
                SizeLimit = sizeLimit,
                SearchRoot = searchRoot
            };

            if (properties.Length > 0)
            {
                searcher.PropertiesToLoad.Clear();
                foreach (var property in properties)
                    if(property is not null)
                        searcher.PropertiesToLoad.Add(property);
            }

            foreach (SearchResult result in searcher.FindAll())
            {
                ResultPropertyCollection rpc = result.Properties;
                list.Add(rpc);
            }

            return Result.Success<IEnumerable<ResultPropertyCollection>>(list);
        }

        /// <summary>
        /// Finds all directory entries matching the specified filter asynchronously.
        /// </summary>
        /// <param name="searchRoot">The search root.</param>
        /// <param name="filter">The search filter.</param>
        /// <param name="searchScope">The search scope.</param>
        /// <param name="sizeLimit">The size limit.</param>
        /// <param name="properties">The properties to load.</param>
        /// <returns>A <see cref="DataResult{T}"/> containing the results.</returns>
        public Task<DataResult<IEnumerable<ResultPropertyCollection>>> FindAllAsync(DirectoryEntry searchRoot, string filter, SearchScope searchScope = SearchScope.Subtree, int sizeLimit = 5000, params string[] properties) => Task.Run(()
            => FindAll(searchRoot, filter, searchScope, sizeLimit, properties));

        /// <summary>
        /// Finds all directory entries matching the specified filter, using the user cache.
        /// </summary>
        /// <param name="username">The username.</param>
        /// <param name="filter">The search filter.</param>
        /// <param name="searchScope">The search scope.</param>
        /// <param name="sizeLimit">The size limit.</param>
        /// <param name="properties">The properties to load.</param>
        /// <returns>A <see cref="DataResult{T}"/> containing the results.</returns>
        public DataResult<IEnumerable<ResultPropertyCollection>> FindAllByUserCache(string username, string filter, SearchScope searchScope = SearchScope.Subtree, int sizeLimit = 5000, params string[] properties)
        {
            _memoryCache.TryGetValue(username, out DirectoryEntry? searchRoot);

            if (searchRoot is null)
                return Result.Fail<IEnumerable<ResultPropertyCollection>>();

            return FindAll(searchRoot, filter, searchScope, sizeLimit, properties);
        }

        /// <summary>
        /// Finds all directory entries matching the specified filter asynchronously, using the user cache.
        /// </summary>
        /// <param name="username">The username.</param>
        /// <param name="filter">The search filter.</param>
        /// <param name="searchScope">The search scope.</param>
        /// <param name="sizeLimit">The size limit.</param>
        /// <param name="properties">The properties to load.</param>
        /// <returns>A <see cref="DataResult{T}"/> containing the results.</returns>
        public Task<DataResult<IEnumerable<ResultPropertyCollection>>> FindAllByUserCacheAsync(string username, string filter, SearchScope searchScope = SearchScope.Subtree, int sizeLimit = 5000, params string[] properties) => Task.Run(()
            => FindAllByUserCache(username, filter, searchScope, sizeLimit, properties));

        /// <summary>
        /// Sets the search root in the cache.
        /// </summary>
        /// <param name="username">The directory entry username.</param>
        /// <param name="password">The directory entry password.</param>
        public void SetSearchRootCache(string username, string password)
        {
            if (_userCacheExpiration is DateTimeOffset cacheExpiration)
                _memoryCache.Set(key: username, new DirectoryEntry(path: SearchRootPath, username: username, password: password), absoluteExpiration: cacheExpiration);
            else
                _memoryCache.Set(key: username, new DirectoryEntry(path: SearchRootPath, username: username, password: password));
        }

        /// <summary>
        /// Gets the search root from the cache.
        /// </summary>
        /// <param name="username">The directory entry username.</param>
        /// <returns>The cached <see cref="DirectoryEntry"/> if found; otherwise, null.</returns>
        public DirectoryEntry? GetSearchRootCache(string username)
        {
            _memoryCache.TryGetValue(username, out DirectoryEntry? root);
            return root;
        }
    }
}