using System;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
using Sockeye.Util;
using Sockeye.Models;
//using System.Diagnostics;


namespace Sockeye.Biz
{

    internal static class NotifyEventHelper
    {
        private static ILogger log = Sockeye.Util.ApplicationLogging.CreateLogger("NotifyEventProcessor");


        ///////////////////////////////////////////
        // ENSURE USER HAS IN APP NOTIFICATION 
        //
        //
        public static async Task EnsureDefaultInAppUserNotificationSubscriptionExists(long userId, AyContext ct)
        {
            var defaultsub = await ct.NotifySubscription.FirstOrDefaultAsync(z => z.EventType == NotifyEventType.GeneralNotification && z.UserId == userId && z.DeliveryMethod == NotifyDeliveryMethod.App);
            if (defaultsub == null)
            {
                //NOTE: agevalue and advanced notice settings here will ensure that direct in app notifications with a future delivery date ("deadman" switch deliveries) set in their 
                //notifyevent.eventdate will deliver on that date and not immediately to support all the things that are direct built in notifications for future dates
                //such as for an overdue Review which doesn't have or need it's own notifyeventtype and subscription independently

                //NEW NOTE: above makes not sense, I'm setting these back to timespan zero
                defaultsub = new NotifySubscription()
                {
                    UserId = userId,
                    EventType = NotifyEventType.GeneralNotification,
                    DeliveryMethod = NotifyDeliveryMethod.App,
                    AgeValue = TimeSpan.Zero,//new TimeSpan(0, 0, 1),
                    AdvanceNotice = TimeSpan.Zero//new TimeSpan(0, 0, 1)
                };
                await ct.NotifySubscription.AddAsync(defaultsub);
                await ct.SaveChangesAsync();
            }
            return;
        }


        /////////////////////////////////////////
        // PROCESS STANDARD EVENTS
        //
        //
        public static async Task ProcessStandardObjectEvents(SockEvent ayaEvent, ICoreBizObjectModel newObject, AyContext ct)
        {
            switch (ayaEvent)
            {
                case SockEvent.Created:
                    await ProcessStandardObjectCreatedEvents(newObject, ct);
                    break;
                case SockEvent.Deleted:
                    await ProcessStandardObjectDeletedEvents(newObject, ct);
                    break;
                case SockEvent.Modified:
                    await ProcessStandardObjectModifiedEvents(newObject, ct);
                    break;
            }
        }

        /////////////////////////////////////////
        // PROCESS STANDARD CREATE NOTIFICATION
        //
        //
        public static async Task ProcessStandardObjectCreatedEvents(ICoreBizObjectModel newObject, AyContext ct)
        {
            //CREATED SUBSCRIPTIONS
            {
                var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.ObjectCreated && z.SockType == newObject.SType).ToListAsync();
                foreach (var sub in subs)
                {
                    //not for inactive users
                    if (!await UserBiz.UserIsActive(sub.UserId)) continue;
                    if (ObjectHasAllSubscriptionTags(newObject.Tags, sub.Tags))
                    {
                        NotifyEvent n = new NotifyEvent()
                        {
                            EventType = NotifyEventType.ObjectCreated,
                            UserId = sub.UserId,
                            SockType = newObject.SType,
                            ObjectId = newObject.Id,
                            NotifySubscriptionId = sub.Id,
                            Name = newObject.Name
                        };
                        await ct.NotifyEvent.AddAsync(n);
                        await ct.SaveChangesAsync();
                        log.LogDebug($"Added NotifyEvent:  [{n.ToString()}]");
                    }
                }
            }


            //AGE SUBSCRIPTIONS
            {
                var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.ObjectAge && z.SockType == newObject.SType).ToListAsync();
                foreach (var sub in subs)
                {
                    //not for inactive users
                    if (!await UserBiz.UserIsActive(sub.UserId)) continue;
                    if (ObjectHasAllSubscriptionTags(newObject.Tags, sub.Tags))
                    {
                        //Note: age is set by advance notice which is consulted by CoreJobNotify in it's run so the deliver date is not required here only the reference EventDate to check for deliver
                        //ObjectAge is determined by subscription AgeValue in combo with the EventDate NotifyEvent parameter which together determines at what age from notifyevent.EventDate it's considered for the event to have officially occured
                        //However delivery is determined by sub.advancenotice so all three values play a part
                        //
                        NotifyEvent n = new NotifyEvent()
                        {
                            EventType = NotifyEventType.ObjectAge,
                            UserId = sub.UserId,
                            SockType = newObject.SType,
                            ObjectId = newObject.Id,
                            NotifySubscriptionId = sub.Id,
                            Name = newObject.Name
                        };
                        await ct.NotifyEvent.AddAsync(n);
                        await ct.SaveChangesAsync();
                        log.LogDebug($"Added NotifyEvent:  [{n.ToString()}]");
                    }
                }
            }


        }


        ///////////////////////////////////////////////
        // PROCESS STANDARD MODIFIED NOTIFICATION
        //
        //
        public static async Task ProcessStandardObjectModifiedEvents(ICoreBizObjectModel newObject, AyContext ct)
        {
            {
                var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.ObjectModified && z.SockType == newObject.SType).ToListAsync();
                foreach (var sub in subs)
                {
                    //not for inactive users
                    if (!await UserBiz.UserIsActive(sub.UserId)) continue;
                    if (ObjectHasAllSubscriptionTags(newObject.Tags, sub.Tags))
                    {
                        NotifyEvent n = new NotifyEvent()
                        {
                            EventType = NotifyEventType.ObjectModified,
                            UserId = sub.UserId,
                            SockType = newObject.SType,
                            ObjectId = newObject.Id,
                            NotifySubscriptionId = sub.Id,
                            Name = newObject.Name
                        };
                        await ct.NotifyEvent.AddAsync(n);
                        await ct.SaveChangesAsync();
                        log.LogDebug($"Added NotifyEvent:  [{n.ToString()}]");
                    }
                }
            }


        }

        /////////////////////////////////////////
        // PROCESS STANDARD DELETE NOTIFICATION
        //
        //
        public static async Task ProcessStandardObjectDeletedEvents(ICoreBizObjectModel bizObject, AyContext ct)
        {
            // It's gone and shouldn't have any events left for it                            
            await ClearPriorEventsForObject(ct, bizObject.SType, bizObject.Id);


            //------------------------------------------
            //ObjectDeleted notification
            //
            {
                var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.ObjectDeleted && z.SockType == bizObject.SType).ToListAsync();
                foreach (var sub in subs)
                {
                    //not for inactive users
                    if (!await UserBiz.UserIsActive(sub.UserId)) continue;
                    if (ObjectHasAllSubscriptionTags(bizObject.Tags, sub.Tags))
                    {
                        //TODO: On deliver should point to history event log record or take from there and insert into delivery message?
                        NotifyEvent n = new NotifyEvent()
                        {
                            EventType = NotifyEventType.ObjectDeleted,
                            UserId = sub.UserId,
                            SockType = bizObject.SType,
                            ObjectId = bizObject.Id,
                            NotifySubscriptionId = sub.Id,
                            Name = bizObject.Name
                        };
                        await ct.NotifyEvent.AddAsync(n);
                        await ct.SaveChangesAsync();
                        log.LogDebug($"Added NotifyEvent:  [{n.ToString()}]");
                    }
                }
            }
        }



        //////////////////////////////////
        // CLEAN OUT OLD EVENTS
        //
        //
        //Any specific event created in objects biz code (not simply "Modified")
        //should trigger this remove code prior to updates etc where it creates a new one
        //particularly for future delivery ones but will catch the case of a quick double edit of an object that 
        //would alter what gets delivered in the notification and before it's sent out yet
        public static async Task ClearPriorEventsForObject(AyContext ct, SockType sockType, long objectId, NotifyEventType eventType)
        {
            var eventsToDelete = await ct.NotifyEvent.Where(z => z.SockType == sockType && z.ObjectId == objectId && z.EventType == eventType).ToListAsync();
            if (eventsToDelete.Count == 0) return;
            ct.NotifyEvent.RemoveRange(eventsToDelete);
            await ct.SaveChangesAsync();
        }

        public static async Task ClearPriorCustomerNotifyEventsForObject(AyContext ct, SockType sockType, long objectId, NotifyEventType eventType)
        {
            var eventsToDelete = await ct.CustomerNotifyEvent.Where(z => z.SockType == sockType && z.ObjectId == objectId && z.EventType == eventType).ToListAsync();
            if (eventsToDelete.Count == 0) return;
            ct.CustomerNotifyEvent.RemoveRange(eventsToDelete);
            await ct.SaveChangesAsync();
        }

        //scorched earth one for outright delete of objects when you don't want any prior events left for it
        //probably only ever used for the delete event, can't think of another one right now new years morning early 2021 a bit hungover but possibly there is :)
        public static async Task ClearPriorEventsForObject(AyContext ct, SockType sockType, long objectId)
        {
            var eventsToDelete = await ct.NotifyEvent.Where(z => z.SockType == sockType && z.ObjectId == objectId).ToListAsync();
            if (eventsToDelete.Count == 0) return;
            ct.NotifyEvent.RemoveRange(eventsToDelete);
            await ct.SaveChangesAsync();
        }


        //////////////////////////////////
        // COMPARE TAGS COLLECTION
        //
        // A match here means *all* tags in the subscription are present in the object
        //
        public static bool ObjectHasAllSubscriptionTags(List<string> objectTags, List<string> subTags)
        {
            //no subscription tags?  Then it always will match
            if (subTags.Count == 0) return true;
            //have sub tags but object has none?  Then it's never going to match
            if (objectTags.Count == 0) return false;

            //not enought tags on object to match sub tags?
            if (subTags.Count > objectTags.Count) return false;

            //Do ALL the tags in the subscription exist in the object?
            return subTags.All(z => objectTags.Any(x => x == z));
        }

        //////////////////////////////////
        // COMPARE TAGS COLLECTION
        //
        // A match here means *all* tags are the same in both objects (don't have to be in same order)
        //
        public static bool TwoObjectsHaveSameTags(List<string> firstObjectTags, List<string> secondObjectTags)
        {
            //no tags on either side?
            if (firstObjectTags.Count == 0 && secondObjectTags.Count == 0) return true;

            //different counts will always mean not a match
            if (firstObjectTags.Count != secondObjectTags.Count) return false;

            //Do ALL the tags in the first object exist in the second object?
            return firstObjectTags.All(z => secondObjectTags.Any(x => x == z));
        }

        /////////////////////////////////////////
        // CREATE OPS PROBLEM EVENT
        // 
        //
        internal static async Task AddOpsProblemEvent(string message, Exception ex = null)
        {
            if (string.IsNullOrWhiteSpace(message) && ex == null)
                return;

            //Log as a backup in case there is no one to notify and also for the record and support
            if (ex != null)
            {
                //actually, if there is an exception it's already logged anyway so don't re-log it here, just makes dupes
                // log.LogError(ex, $"Ops problem notification: \"{message}\"");
                message += $"\nException error: {ExceptionUtil.ExtractAllExceptionMessages(ex)}";
            }
            else
                log.LogWarning($"Ops problem notification: \"{message}\"");

            await AddGeneralNotifyEvent(NotifyEventType.ServerOperationsProblem, message, "OPS");
        }


        /////////////////////////////////////////
        // CREATE GENERAL NOTIFY EVENT
        //
        //

        internal static async Task AddGeneralNotifyEvent(NotifyEventType eventType, string message, string name, Exception except = null, long userId = 0)
        {
            await AddGeneralNotifyEvent(SockType.NoType, 0, eventType, message, name, except, userId);
        }
        internal static async Task AddGeneralNotifyEvent(SockType sockType, long objectid, NotifyEventType eventType, string message, string name, Exception except = null, long userId = 0)
        {

            //This handles general notification events not requiring a decision or tied to an object that are basically just a immediate message to the user
            //e.g. ops problems, GeneralNotification, NotifyHealthCheck etc
            //optional user id to send directly to them
            log.LogDebug($"AddGeneralNotifyEvent processing:  [type:{eventType}, userId:{userId}, message:{message}]");
#if (DEBUG)
            switch (eventType)
            {
                case NotifyEventType.BackupStatus:
                case NotifyEventType.GeneralNotification:
                case NotifyEventType.NotifyHealthCheck://created by job processor itself
                case NotifyEventType.ServerOperationsProblem:
                    break;
                default://this will likely be a development error, not a production error so no need to log etc
                    throw (new System.NotSupportedException($"NotifyEventProcessor:AddGeneralNotifyEvent - Type of event {eventType} is unexpected and not supported"));
            }

            if (eventType != NotifyEventType.GeneralNotification && userId != 0)
            {
                throw (new System.NotSupportedException($"NotifyEventProcessor:AddGeneralNotifyEvent - event {eventType} was specified with user id {userId} which is unexpected and not supported"));
            }
#endif

            try
            {
                using (AyContext ct = Sockeye.Util.ServiceProviderProvider.DBContext)
                {

                    //General notification goes to one specific user only                  
                    if (eventType == NotifyEventType.GeneralNotification)
                    {
                        if (userId == 0)
                        {
                            //this will likely be a development error, not a production error so no need to log etc
                            throw new System.ArgumentException("NotifyEventProcessor:AddGeneralNotifyEvent: GeneralNotification requires a user id but none was specified");
                        }
                        //not for inactive users
                        if (!await UserBiz.UserIsActive(userId)) return;

                        var UserName = await ct.User.AsNoTracking().Where(z => z.Id == userId).Select(z => z.Name).FirstOrDefaultAsync();

                        //if they don't have a regular inapp subscription create one now
                        await EnsureDefaultInAppUserNotificationSubscriptionExists(userId, ct);

                        if (string.IsNullOrWhiteSpace(name))
                            name = UserName;

                        var gensubs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.GeneralNotification && z.UserId == userId).ToListAsync();
                        foreach (var sub in gensubs)
                        {
                            NotifyEvent n = new NotifyEvent() { EventType = eventType, UserId = userId, Message = message, NotifySubscriptionId = sub.Id, Name = name, SockType = sockType, ObjectId = objectid };
                            await ct.NotifyEvent.AddAsync(n);
                        }
                        if (gensubs.Count > 0)
                            await ct.SaveChangesAsync();
                        return;
                    }


                    //check subscriptions for event and send accordingly to each user
                    var subs = await ct.NotifySubscription.Where(z => z.EventType == eventType).ToListAsync();

                    //append exception message if not null
                    if (except != null)
                        message += $"\nException error: {ExceptionUtil.ExtractAllExceptionMessages(except)}";

                    foreach (var sub in subs)
                    {
                        //not for inactive users
                        if (!await UserBiz.UserIsActive(sub.UserId)) continue;
                        //note flag ~SERVER~ means to client to substitute "Server" translation key text instead
                        NotifyEvent n = new NotifyEvent() { EventType = eventType, UserId = sub.UserId, Message = message, NotifySubscriptionId = sub.Id, Name = "~SERVER~", SockType = sockType, ObjectId = objectid };
                        await ct.NotifyEvent.AddAsync(n);
                    }
                    if (subs.Count > 0)
                        await ct.SaveChangesAsync();
                }

            }
            catch (Exception ex)
            {
                log.LogError(ex, $"Error adding general notify event [type:{eventType}, userId:{userId}, message:{message}]");
                DbUtil.HandleIfDatabaseUnavailableTypeException(ex);
            }

        }//eom



    }//eoc

}//eons
