using Disco.Data.Repository;
using Disco.Models.Repository;
using Disco.Services.Logging;
using Disco.Services.Tasks;
using Quartz;
using System;
using System.Collections;
using System.Collections.Generic;
using System.DirectoryServices;
using System.DirectoryServices.ActiveDirectory;
using System.Linq;

namespace Disco.Services.Interop.ActiveDirectory
{
    public class ADNetworkLogonDatesUpdateTask : ScheduledTask
    {
        public override string TaskName { get { return "Active Directory - Update Last Network Logon Dates Task"; } }
        public override bool SingleInstanceTask { get { return true; } }
        public override bool CancelInitiallySupported { get { return false; } }

        public override void InitalizeScheduledTask(DiscoDataContext Database)
        {
            // ADNetworkLogonDatesUpdateTask @ 11:30pm
            TriggerBuilder triggerBuilder = TriggerBuilder.Create().
                WithSchedule(CronScheduleBuilder.DailyAtHourAndMinute(23, 30));

            this.ScheduleTask(triggerBuilder);
        }

        protected override void ExecuteTask()
        {
            int changeCount;

            this.Status.UpdateStatus(1, "Starting", "Connecting to the Database and initializing the environment");
            using (DiscoDataContext database = new DiscoDataContext())
            {
                UpdateLastNetworkLogonDates(database, this.Status);
                this.Status.UpdateStatus(95, "Updating Database", "Writing last network logon dates to the Database");
                changeCount = database.SaveChanges();
                this.Status.Finished(string.Format("{0} Device last network logon dates updated", changeCount), "/Config/SystemConfig");
            }

            SystemLog.LogInformation(new string[]
                {
                    "Updated LastNetworkLogon Device Property for Device/s", 
                    changeCount.ToString()
                });
        }

        public static ScheduledTaskStatus ScheduleImmediately()
        {
            var existingTask = ScheduledTasks.GetTaskStatuses(typeof(ADNetworkLogonDatesUpdateTask)).Where(s => s.IsRunning).FirstOrDefault();
            if (existingTask != null)
                return existingTask;

            var instance = new ADNetworkLogonDatesUpdateTask();
            return instance.ScheduleTask();
        }

        public static bool UpdateLastNetworkLogonDate(Device Device)
        {
            const string ldapFilterTemplate = "(&(objectCategory=Computer)(sAMAccountName={0}))";
            string[] ldapProperties = new string[] { "lastLogon", "lastLogonTimestamp" };

            System.DateTime? lastLogon = null;

            if (!string.IsNullOrEmpty(Device.DeviceDomainId) && Device.DeviceDomainId.Contains('\\'))
            {
                var context = ActiveDirectory.Context;
                string deviceSamAccountName;
                ADDomain deviceDomain;

                ActiveDirectory.ParseDomainAccountId(Device.DeviceDomainId + "$", out deviceSamAccountName, out deviceDomain);
                
                var ldapFilter = string.Format(ldapFilterTemplate, ADHelpers.EscapeLdapQuery(deviceSamAccountName));
                IEnumerable<ADDomainController> domainControllers;

                if (context.SearchAllForestServers)
                    domainControllers = deviceDomain.GetAllReachableDomainControllers();
                else
                    domainControllers = deviceDomain.GetReachableSiteDomainControllers();

                lastLogon = domainControllers.Select(dc =>
                {
                    var result = dc.SearchEntireDomain(ldapFilter, ldapProperties, ActiveDirectory.SingleSearchResult).FirstOrDefault();

                    if (result != null)
                    {
                        long lastLogonValue = default(long);
                        long lastLogonTimestampValue = default(long);

                        lastLogonValue = result.Value<long>("lastLogon");
                        lastLogonTimestampValue = result.Value<long>("lastLogonTimestamp");

                        long highedValue = Math.Max(lastLogonValue, lastLogonTimestampValue);

                        if (highedValue > 0)
                            return (DateTime?)new DateTime((DateTime.FromFileTime(highedValue).Ticks / 10000000L) * 10000000L);
                        else
                            return null;
                    }
                    return null;
                }).Where(dt => dt.HasValue).Max();
            }

            if (lastLogon.HasValue &&
                (
                !Device.LastNetworkLogonDate.HasValue
                || Device.LastNetworkLogonDate.Value < lastLogon
                ))
            {
                Device.LastNetworkLogonDate = lastLogon;
                return true;
            }
            return false;
        }

        public static void UpdateLastNetworkLogonDates(DiscoDataContext Database, IScheduledTaskStatus status)
        {
            var context = ActiveDirectory.Context;
            const string ldapFilter = "(objectCategory=Computer)";
            string[] ldapProperties = new string[] { "sAMAccountName", "lastLogon" };

            status.UpdateStatus(2, "Initializing", "Determining Domains and Available Domain Controllers");

            // Determine Domain Scopes to Query
            var domainQueries = context.Domains
                .Select(d => Tuple.Create(d, d.SearchContainers ?? new List<string>() { d.DistinguishedName }))
                .Where(d => d.Item2.Count > 0);

            // Determine Domain Controllers to Query
            IEnumerable<Tuple<ADDomain, ADDomainController, List<string>>> serverQueries;
            if (context.SearchAllForestServers)
                serverQueries = domainQueries.SelectMany(q => q.Item1.GetAllReachableDomainControllers(), (q, dc) => Tuple.Create(q.Item1, dc, q.Item2));
            else
                serverQueries = domainQueries.SelectMany(q => q.Item1.GetReachableSiteDomainControllers(), (q, dc) => Tuple.Create(q.Item1, dc, q.Item2));

            var scopedQueries = serverQueries.SelectMany(q => q.Item3, (q, scope) => Tuple.Create(q.Item1, q.Item2, scope)).ToList();

            var queries = Enumerable.Range(0, scopedQueries.Count).Select(i =>
            {
                var q = scopedQueries[i];
                return Tuple.Create(i, q.Item1, q.Item2, q.Item3);
            });

            var queryResults = queries.SelectMany(q =>
            {
                var queryIndex = q.Item1;
                var domain = q.Item2;
                var domainController = q.Item3;
                var searchRoot = q.Item4;

                // Update Status
                double progress = 5 + (queryIndex * (90 / scopedQueries.Count));
                status.UpdateStatus(progress, string.Format("Querying Domain [{0}] using controller [{1}]", domain.NetBiosName, domainController.Name), string.Format("Searching: {0}", searchRoot));

                // Perform Query
                var directoryResults = domainController.SearchInternal(searchRoot, ldapFilter, ldapProperties, null);

                return directoryResults.Select(result =>
                {
                    var samAccountName = result.Value<string>("sAMAccountName");

                    long lastLogonValue = default(long);
                    long lastLogonTimestampValue = default(long);

                    lastLogonValue = result.Value<long>("lastLogon");
                    lastLogonTimestampValue = result.Value<long>("lastLogonTimestamp");

                    long highedValue = Math.Max(lastLogonValue, lastLogonTimestampValue);

                    if (highedValue > 0)
                    {
                        var computerName = string.Format(@"{0}\{1}", domain.NetBiosName, samAccountName.TrimEnd('$'));
                        var lastLogon = new DateTime((DateTime.FromFileTime(highedValue).Ticks / 10000000L) * 10000000L);
                        return Tuple.Create(computerName, lastLogon);
                    }
                    else
                        return null;
                }).Where(i => i != null).ToList();

            }).GroupBy(r => r.Item1, StringComparer.OrdinalIgnoreCase).ToDictionary(g => g.Key.ToUpper(), g => g.Max(i => i.Item2));

            status.UpdateStatus(90, "Processing Results", "Processing last network logon dates and looking for updates");

            foreach (Device device in Database.Devices.Where(device => device.DeviceDomainId != null))
            {
                DateTime lastLogonDate;
                if (queryResults.TryGetValue(device.DeviceDomainId.ToUpper(), out lastLogonDate))
                {
                    if (!device.LastNetworkLogonDate.HasValue)
                        device.LastNetworkLogonDate = lastLogonDate;
                    else
                    {
                        // Change accuracy to the second
                        lastLogonDate = new DateTime((lastLogonDate.Ticks / 10000000L) * 10000000L);

                        if (device.LastNetworkLogonDate.Value < lastLogonDate)
                            device.LastNetworkLogonDate = lastLogonDate;
                    }
                }
            }
        }

    }
}
