using System;
using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using AyaNova.Models;
using AyaNova.Util;

namespace AyaNova.Biz
{

    /// <summary>
    /// Notification processor
    /// turn notifyEvent records into notifications for in app and deliver smtp
    /// 
    /// </summary>
    internal static class CoreJobNotify
    {
        private static bool NotifyIsRunning = false;
        private static ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger("CoreJobNotify");



        private static DateTime lastRun = DateTime.MinValue;
        // private static TimeSpan DELETE_AFTER_AGE = new TimeSpan(90, 0, 0, 0);
        // private static TimeSpan RUN_EVERY_INTERVAL = new TimeSpan(0, 2, 0);//once every 2 minutes minimum
        private static DateTime lastNotifyHealthCheckSentLocal = DateTime.MinValue;
        private static TimeSpan TS_24_HOURS = new TimeSpan(24, 0, 0);//used to ensure daily ops happen no more than that

#if (DEBUG)
        private static TimeSpan DELETE_AFTER_AGE = new TimeSpan(0, 12, 0, 0);
        private static TimeSpan RUN_EVERY_INTERVAL = new TimeSpan(0, 0, 20);//no more frequently than once every 20 seconds
#else
 private static TimeSpan DELETE_AFTER_AGE = new TimeSpan(90, 0, 0, 0);
        private static TimeSpan RUN_EVERY_INTERVAL = new TimeSpan(0, 2, 0);//no more frequently than once every 2 minutes
#endif

        //temporary list to hold translations as required during delivery
        private static Dictionary<long, List<NameIdItem>> _NotifyEventTypeTransCache = new Dictionary<long, List<NameIdItem>>();
        private static Dictionary<long, List<NameIdItem>> _AyaTypeTypeTransCache = new Dictionary<long, List<NameIdItem>>();
        private static Dictionary<long, long> _UserTranslationIdCache = new Dictionary<long, long>();
        private static Dictionary<long, string> _ServerTheWordTranslations = new Dictionary<long, string>();

        ////////////////////////////////////////////////////////////////////////////////////////////////
        // DoSweep
        //
        public static async Task DoWorkAsync()
        {
            log.LogTrace("Checking if Notify should run");
            if (NotifyIsRunning)
            {
                log.LogTrace("Notify is running already exiting this cycle");
                return;
            }
            //This will get triggered roughly every minute, but we don't want to deliver that frequently
            if (DateTime.UtcNow - lastRun < RUN_EVERY_INTERVAL)
            {
                log.LogTrace($"Notify ran less than {RUN_EVERY_INTERVAL}, exiting this cycle");
                return;
            }
            try
            {
                NotifyIsRunning = true;
                log.LogTrace("Notify set to RUNNING state and starting now");

                //NotifyHealthCheck processing
                //Note this deliberately uses LOCAL time in effort to deliver the health check first thing in the morning for workers
                //However if server is on UTC already then that's what is used, there is no adjustment
                DateTime dtNowLocal = DateTime.Now;
                if (dtNowLocal - lastNotifyHealthCheckSentLocal > TS_24_HOURS)
                {
                    //are we in the 7th to 9th hour?
                    if (dtNowLocal.Hour > 6 && dtNowLocal.Hour < 10)
                    {
                        log.LogTrace("Notify health check submitted to subscribers");
                        await NotifyEventProcessor.AddGeneralNotifyEvent(NotifyEventType.NotifyHealthCheck, "OK", "");
                        lastNotifyHealthCheckSentLocal = dtNowLocal;
                    }
                }




                using (AyContext ct = AyaNova.Util.ServiceProviderProvider.DBContext)
                {
                    //select all jobs with no deliver date or deliver date no longer in future

                    //IMPLEMENTATION NOTES:  This code
                    // finds the events in NotifyEvent
                    // check subscription to determine if the age related ones are deliverable now
                    // check subscriptions for each delivery type, i.e. there might be both smtp and inapp separately for the same event so it needs to know that and deliver accordingly
                    // remove the notify event after it's processed



                    //Older notes:
                    //### PLAN if it's an smtp delivery that fails and it's to someone who can be delivered in app then it should send an inapp notification of 
                    //delivery failure and still delete the smtp delivery
                    //If it's not possible to notify the person via in app of the failed smtp then perhaps it notifies OPS personnel and biz admin personnel
                    //NEW NOTIFICATION SUBSCRIPTION EVENT TYPE:
                    //OPERATIONS_PROBLEMS - backup, notifications, out of memory, what have you, anyone can subscribe to it regardless of rights
                    //this is just to let people know there is a problem
                    //todo: create message here if not already set?
                    //todo: Link to open report in here
                    //CLIENT EXPECTS: open report links to have a query string [CLIENTAPPURL]/viewreport?oid=[objectid]&rid=[reportid]


                    //All items have an event date, for non time delayed events it's just the moment it was created
                    //which will predate this moment now if it's pre-existing
                    var events = await ct.NotifyEvent.Include(z => z.NotifySubscription).ToListAsync();
                    log.LogTrace($"Found {events.Count} NotifyEvents to examine for potential delivery");

                    //cache translations
                    //Get all subscription unique userId's that aren't inapp deliveries
                    var usersNeedingTranslations = events.Where(z => z.NotifySubscription.DeliveryMethod != NotifyDeliveryMethod.App).Select(z => z.NotifySubscription.UserId).ToList().Distinct();
                    foreach (long userid in usersNeedingTranslations)
                    {
                        long transId = (await ct.UserOptions.SingleAsync(z => z.UserId == userid)).TranslationId;
                        _UserTranslationIdCache.Add(userid, transId);
                        await CacheTranslations(transId);
                    }
                    //cache all translations of the word "Server" for server notifications
                    _ServerTheWordTranslations = await TranslationBiz.GetAllTranslationsForKey("Server");

                    //iterate and deliver
                    foreach (var notifyevent in events)
                    {
                        //TIME DELAYED AGED EVENT?
                        //when to time delay deliver formula:If sub.agevalue!= timespan.zero then deliver on = 
                        //NotifyEvent "EventDate"+NotifySubscription.AgeValue timespan - NotifySubscription AdvanceNotice timespan > utcNow
                        //Is it time delayed?
                        if (notifyevent.NotifySubscription.AgeValue != TimeSpan.Zero)
                        {
                            var deliverAfter = notifyevent.EventDate + notifyevent.NotifySubscription.AgeValue - notifyevent.NotifySubscription.AdvanceNotice;
                            if (deliverAfter < DateTime.UtcNow)
                            {
                                if (notifyevent.NotifySubscription.DeliveryMethod == NotifyDeliveryMethod.App)
                                {
                                    await DeliverInApp(notifyevent, ct);
                                }
                                if (notifyevent.NotifySubscription.DeliveryMethod == NotifyDeliveryMethod.SMTP)
                                {
                                    await DeliverSMTP(notifyevent, ct);
                                }
                            }
                        }
                        else
                        {
                            //NORMAL IMMEDIATE DELIVERY EVENT
                            if (notifyevent.NotifySubscription.DeliveryMethod == NotifyDeliveryMethod.App)
                            {
                                await DeliverInApp(notifyevent, ct);
                            }
                            if (notifyevent.NotifySubscription.DeliveryMethod == NotifyDeliveryMethod.SMTP)
                            {
                                await DeliverSMTP(notifyevent, ct);
                            }
                        }
                    }

                }
            }
            catch (Exception ex)
            {
                log.LogError(ex, $"Error processing notification event");
            }
            finally
            {
                log.LogTrace("Notify is done setting to not running state and tagging lastRun timestamp");
                _UserTranslationIdCache.Clear();
                _NotifyEventTypeTransCache.Clear();
                _AyaTypeTypeTransCache.Clear();
                lastRun = DateTime.UtcNow;
                NotifyIsRunning = false;

            }

        }

        //cache any translations required for email notification
        private static async Task CacheTranslations(long translationId)
        {
            if (!_NotifyEventTypeTransCache.ContainsKey(translationId))
                _NotifyEventTypeTransCache.Add(translationId, await AyaNova.Api.Controllers.EnumListController.GetEnumList("NotifyEventType", translationId));

            if (!_AyaTypeTypeTransCache.ContainsKey(translationId))
                _AyaTypeTypeTransCache.Add(translationId, await AyaNova.Api.Controllers.EnumListController.GetEnumList("AyaType", translationId));

        }

        //Used for subject of email and message deliveries
        private static string GetTranslatedNotifyEventName(NotifyEventType net, long translationId)
        {
            return _NotifyEventTypeTransCache.First(z => z.Key == translationId).Value.First(z => z.Id == (int)net).Name;
        }

        private static string GetTranslatedAyaTypeName(AyaType at, long translationId)
        {
            return _AyaTypeTypeTransCache.First(z => z.Key == translationId).Value.First(z => z.Id == (int)at).Name;
        }

        private static async Task DeliverInApp(NotifyEvent ne, AyContext ct)
        {
            log.LogTrace($"DeliverInApp deliving notify event: {ne}");
            await ct.Notification.AddAsync(new Notification() { UserId = ne.UserId, AyaType = ne.AyaType, ObjectId = ne.ObjectId, EventType = ne.EventType, NotifySubscriptionId = ne.NotifySubscriptionId, Message = ne.Message, Name = ne.Name });
            ct.NotifyEvent.Remove(ne);
            await ct.SaveChangesAsync();
        }



        private static async Task DeliverSMTP(NotifyEvent ne, AyContext ct)
        {
            log.LogTrace($"DeliverSMTP delivering notify event: {ne}");
            if (string.IsNullOrWhiteSpace(ne.NotifySubscription.DeliveryAddress))
                await NotifyEventProcessor.AddGeneralNotifyEvent(NotifyEventType.GeneralNotification, $"No email address is set in subscription to deliver email notification for this event:{ne}", "Error", null, ne.UserId);




            var transid = _UserTranslationIdCache[ne.NotifySubscription.UserId];
            var name = ne.Name;
            if (name == "~SERVER~")
                name = _ServerTheWordTranslations.First(z => z.Key == transid).Value;


            //AyaType translation
            var AyaTypeTranslated = string.Empty;
            if (ne.AyaType != AyaType.NoType)
                AyaTypeTranslated = $":{GetTranslatedAyaTypeName(ne.AyaType, transid)}";



            var subject = $"AY{AyaTypeTranslated}:{GetTranslatedNotifyEventName(ne.EventType, transid)}:{name}";

            IMailer m = AyaNova.Util.ServiceProviderProvider.Mailer;
            try
            {
                var body = "";
                //NOTE: if need any other exemptions besides backup status make a separate static function "CanOpen(NotifyEventType)"

                if (ne.ObjectId != 0 || ne.EventType == NotifyEventType.BackupStatus)
                {
                    body = OpenObjectUrlBuilder(ne.AyaType, ne.ObjectId, ne.EventType) + "\n";
                }
                body += ne.Message;

                if (!ServerGlobalOpsSettingsCache.Notify.SmtpDeliveryActive)
                {
                    await NotifyEventProcessor.AddGeneralNotifyEvent(NotifyEventType.GeneralNotification, $"Email notifications are set to OFF at server, unable to send email notification for this event:{ne}", "Error", null, ne.UserId);
                    log.LogInformation($"** WARNING: SMTP notification is currently set to Active=False; unable to deliver email notification, re-routed to in-app notification instead [UserId={ne.UserId}, Notify subscription={ne.NotifySubscriptionId}].  Change this setting or have users remove email delivery notifications if this is permanent **");
                }
                else
                {
                    await m.SendEmailAsync(ne.NotifySubscription.DeliveryAddress, subject, body, ServerGlobalOpsSettingsCache.Notify);
                }
            }
            catch (Exception ex)
            {
                await NotifyEventProcessor.AddOpsProblemEvent("SMTP Notification failed", ex);
                await NotifyEventProcessor.AddGeneralNotifyEvent(NotifyEventType.GeneralNotification, $"An error prevented delivering the following notification via email. System operator users have been notified:{ne}", "Error", null, ne.UserId);
            }
            finally
            {
                //remove event no matter what
                ct.NotifyEvent.Remove(ne);
                await ct.SaveChangesAsync();
            }
        }

        //Called from ops notification settings to test smtp setup by delivering to address of choosing
        public static async Task<string> TestSMTPDelivery(string toAddress)
        {
            //DO TEST DELIVERY HERE USING EXACT SAME SETTINGS AS FOR DeliverSMTP above
            //todo: abstract out email sending to it's own class maybe or whatever method I choose supports the best
            //https://jasonwatmore.com/post/2020/07/15/aspnet-core-3-send-emails-via-smtp-with-mailkit
            //https://medium.com/@ffimnsr/sending-email-using-mailkit-in-asp-net-core-web-api-71b946380442

            IMailer m = AyaNova.Util.ServiceProviderProvider.Mailer;
            try
            {
                await m.SendEmailAsync(toAddress, "Test from Notification system", "This is a test to confirm notification system is working", ServerGlobalOpsSettingsCache.Notify);
                return "ok";
            }
            catch (Exception ex)
            {
                await NotifyEventProcessor.AddOpsProblemEvent("SMTP (TEST) Notification failed", ex);
                return ExceptionUtil.ExtractAllExceptionMessages(ex);
            }

        }


        //Open object url
        //### NOTE: If this is required anywhere else, move it to a central BizUtils class in Biz folder callable from here and there 
        private static string OpenObjectUrlBuilder(AyaType otype, long id, NotifyEventType net)
        {
            var ServerUrl = ServerGlobalOpsSettingsCache.Notify.AyaNovaServerURL;
            if (string.IsNullOrWhiteSpace(ServerUrl))
            {
                NotifyEventProcessor.AddOpsProblemEvent("Notification system: The OPS Notification setting is empty for AyaNova Server URL.  This prevents Notification system from linking events to openable objects.").Wait();
                return "OPS ERROR NO SERVER URL CONFIGURED";
            }
            ServerUrl = ServerUrl.Trim().TrimEnd('/');

            //HANDLE ITEMS WITHOUT TYPE OR ID
            if (net == NotifyEventType.BackupStatus)
            {
                return $"{ServerUrl}/ops-backup";
            }


            //Might not have a type or id in which case nothing directly to open
            if (otype == AyaType.NoType || id == 0)
            {
                return ServerUrl;
            }

            if (NotifyEventType.ObjectDeleted == net)
            {
                //goto event log for item
                // path: "/history/:ayatype/:recordid/:userlog?",
                return $"{ServerUrl}/history/{(int)otype}/{id}";
            }

            //default is to open the object in question directly            
            return $"{ServerUrl}/open/{(int)otype}/{id}";
        }


        //Used to directly open a report at client
        private static string OpenReportUrlBuilder(long objectId, long reportId)
        {
            //CLIENT EXPECTS: open report links to have a query string [CLIENTAPPURL]/viewreport?oid=[objectid]&rid=[reportid]
            var ServerUrl = ServerGlobalOpsSettingsCache.Notify.AyaNovaServerURL;
            if (string.IsNullOrWhiteSpace(ServerUrl))
            {
                NotifyEventProcessor.AddOpsProblemEvent("Notification system: The OPS Notification setting is empty for AyaNova Server URL.  This prevents Notification system from linking events to openable objects.").Wait();
                return "OPS ERROR NO SERVER URL CONFIGURED";
            }
            ServerUrl = ServerUrl.Trim().TrimEnd('/');
            return $"{ServerUrl}/viewreport?oid={objectId}&rid={reportId}";
        }

        /////////////////////////////////////////////////////////////////////

    }//eoc


}//eons

