using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System.Linq;
using Sockeye.Util;
using Sockeye.Api.ControllerHelpers;
using Sockeye.Models;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using Newtonsoft.Json;

namespace Sockeye.Biz
{
    internal class ReviewBiz : BizObject, IJobObject, ISearchAbleObject, IReportAbleObject, IExportAbleObject, IImportAbleObject, INotifiableObject
    {
        internal ReviewBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles)
        {
            ct = dbcontext;
            UserId = currentUserId;
            UserTranslationId = userTranslationId;
            CurrentUserRoles = UserRoles;
            BizType = SockType.Review;
        }

        internal static ReviewBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
        {
            if (httpContext != null)
                return new ReviewBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
            else
                return new ReviewBiz(ct, 1, ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin);
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////
        //EXISTS 
        internal async Task<bool> ExistsAsync(long id)
        {
            return await ct.Review.AnyAsync(z => z.Id == id);
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////
        //CREATE   
        //
        internal async Task<Review> CreateAsync(Review newObject)
        {
            await ValidateAsync(newObject, null);
            if (HasErrors)
                return null;
            else
            {
                newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
                newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
                await ct.Review.AddAsync(newObject);
                await ct.SaveChangesAsync();
                await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, SockEvent.Created), ct);
                await SearchIndexAsync(newObject, true);
                await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
                await HandlePotentialNotificationEvent(SockEvent.Created, newObject);
                await PopulateVizFields(newObject);
                return newObject;
            }
        }



        ////////////////////////////////////////////////////////////////////////////////////////////////
        //GET 
        //      
        internal async Task<Review> GetAsync(long id, bool logTheGetEvent = true)
        {
            var ret = await ct.Review.AsNoTracking().SingleOrDefaultAsync(m => m.Id == id);
            if (logTheGetEvent && ret != null)
                await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, SockEvent.Retrieved), ct);
            await PopulateVizFields(ret);
            return ret;
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////
        //UPDATE
        //
        internal async Task<Review> PutAsync(Review putObject)
        {
            var dbObject = await GetAsync(putObject.Id, false);
            if (dbObject == null)
            {
                AddError(ApiErrorCode.NOT_FOUND, "id");
                return null;
            }
            if (dbObject.Concurrency != putObject.Concurrency)
            {
                AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
                return null;
            }

            putObject.Tags = TagBiz.NormalizeTags(putObject.Tags);
            putObject.CustomFields = JsonUtil.CompactJson(putObject.CustomFields);
            await ValidateAsync(putObject, dbObject);
            if (HasErrors) return null;
            ct.Replace(dbObject, putObject);
            try
            {
                await ct.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!await ExistsAsync(putObject.Id))
                    AddError(ApiErrorCode.NOT_FOUND);
                else
                    AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
                return null;
            }
            await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, BizType, SockEvent.Modified), ct);
            await SearchIndexAsync(putObject, false);
            await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags);
            await HandlePotentialNotificationEvent(SockEvent.Modified, putObject, dbObject);
            await PopulateVizFields(putObject);
            return putObject;
        }


        ////////////////////////////////////////////////////////////////////////////////////////////////
        //UPDATE schedule only
        //       
        internal async Task<bool> PutNewScheduleTimeAsync(ScheduleItemAdjustParams p)
        {
            Review dbObject = await ct.Review.SingleOrDefaultAsync(z => z.Id == p.Id);
            if (dbObject == null)
            {
                AddError(ApiErrorCode.NOT_FOUND, "id");
                return false;
            }

            dbObject.ReviewDate = p.Start;

            await ValidateAsync(dbObject, dbObject);
            if (HasErrors) return false;
            try
            {
                await ct.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!await ExistsAsync(dbObject.Id))
                    AddError(ApiErrorCode.NOT_FOUND);
                else
                    AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
                return false;
            }
            await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, dbObject.SockType, SockEvent.Modified), ct);

            await HandlePotentialNotificationEvent(SockEvent.Modified, dbObject, dbObject);
            return true;
        }


        ////////////////////////////////////////////////////////////////////////////////////////////////
        //DELETE
        //
        internal async Task<bool> DeleteAsync(long id, Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction parentTransaction = null)
        {
            Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction transaction = null;
            if (parentTransaction == null)
                transaction = await ct.Database.BeginTransactionAsync();
            var dbObject = await GetAsync(id, false);
            if (dbObject == null)
            {
                AddError(ApiErrorCode.NOT_FOUND);
                return false;
            }
            ValidateCanDelete(dbObject);
            if (HasErrors)
                return false;
            ct.Review.Remove(dbObject);
            await ct.SaveChangesAsync();

            //Log event 
            await EventLogProcessor.DeleteObjectLogAsync(UserId, BizType, dbObject.Id, dbObject.Name, ct);
            await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, BizType, ct);
            await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
            await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct);
            //all good do the commit if it's ours
            if (parentTransaction == null)
                await transaction.CommitAsync();
            await HandlePotentialNotificationEvent(SockEvent.Deleted, dbObject);
            return true;

        }


        ////////////////////////////////////////////////////////////////////////////////////////////////
        //SEARCH
        //
        private async Task SearchIndexAsync(Review obj, bool isNew)
        {
            var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, BizType);
            DigestSearchText(obj, SearchParams);
            if (isNew)
                await Search.ProcessNewObjectKeywordsAsync(SearchParams);
            else
                await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams);
        }

        public async Task<Search.SearchIndexProcessObjectParameters> GetSearchResultSummary(long id, SockType specificType)
        {
            var obj = await GetAsync(id, false);
            var SearchParams = new Search.SearchIndexProcessObjectParameters();
            DigestSearchText(obj, SearchParams);
            return SearchParams;
        }

        public void DigestSearchText(Review obj, Search.SearchIndexProcessObjectParameters searchParams)
        {
            if (obj != null)
                searchParams.AddText(obj.Notes)
                            .AddText(obj.Name)
                            .AddText(obj.Wiki)
                            .AddText(obj.Tags)
                            .AddText(obj.CompletionNotes)
                            .AddCustomFields(obj.CustomFields);
        }



        ////////////////////////////////////////////////////////////////////////////////////////////////
        //VALIDATION
        //

        private async Task ValidateAsync(Review proposedObj, Review currentObj)
        {
            /*
            - RULE Roles: BizAdmin, Service, Inventory, Accounting, Sales can create and assign to anyone else. 
            - RULE Any other inside role can create for themselves only. (outside roles have no rights to this object so no need to check)
            - RULE Restricted roles can only set completed date and enter completion notes not otherwise change or create or delete.
            - BIZ RULE users with more than restricted roles can assign other users  
            */

            bool isNew = currentObj == null;
            bool SelfAssigned = proposedObj.AssignedByUserId == UserId && proposedObj.UserId == UserId;
            bool HasSupervisorRole =
            CurrentUserRoles.HasFlag(AuthorizationRoles.BizAdmin) ||
            CurrentUserRoles.HasFlag(AuthorizationRoles.Service) ||
            CurrentUserRoles.HasFlag(AuthorizationRoles.Inventory) ||
            CurrentUserRoles.HasFlag(AuthorizationRoles.Sales) ||
            CurrentUserRoles.HasFlag(AuthorizationRoles.Accounting);

            //Checks for non supervisors
            if (!HasSupervisorRole)
            {
                //Non supervisor can't create a Review and assign to other User
                if (isNew && !SelfAssigned)
                {
                    AddError(ApiErrorCode.NOT_AUTHORIZED, "UserId");
                    return;//no need to check any further this is disqualifying completely
                }

                //Non supervisory roles can only change / set certain fields for non self reviews
                if (!isNew && !SelfAssigned)
                {
                    if (
                        (currentObj.Name != proposedObj.Name) ||
                        (currentObj.Notes != proposedObj.Notes) ||
                        (currentObj.ReviewDate != proposedObj.ReviewDate) ||
                        (currentObj.UserId != proposedObj.UserId) ||
                        (currentObj.AssignedByUserId != proposedObj.AssignedByUserId))
                    {
                        AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror");
                        return;
                    }
                }
            }

            //Can't change assigned object id and type after initial save
            if (!isNew)
            {
                if (proposedObj.ObjectId != currentObj.ObjectId)
                {
                    AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ObjectId");
                    return;
                }
                if (proposedObj.SockType != currentObj.SockType)
                {
                    AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "SockType");
                    return;
                }
            }

            //Can't change Review date after completed
            //user must set empty completed before changing start date if they really want to do this
            if (!isNew && proposedObj.CompletedDate != null)
            {
                if (proposedObj.ReviewDate != currentObj.ReviewDate)
                {
                    AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ReviewDate");
                    return;
                }
            }

            //Does the object of this Review actually exist?
            if (!await BizObjectExistsInDatabase.ExistsAsync(proposedObj.SockType, proposedObj.ObjectId, ct))
            {
                AddError(ApiErrorCode.NOT_FOUND, "generalerror", $"LT:ErrorAPI2010 LT:{proposedObj.SockType} id {proposedObj.ObjectId}");
                return;
            }

            //Name required
            if (string.IsNullOrWhiteSpace(proposedObj.Name))
                AddError(ApiErrorCode.VALIDATION_REQUIRED, "Name");

            //Any form customizations to validate?
            var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(x => x.FormKey == SockType.Review.ToString());
            if (FormCustomization != null)
            {
                //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required

                //validate users choices for required non custom fields
                RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);

                //validate custom fields
                CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields);
            }

        }

        private void ValidateCanDelete(Review inObj)
        {
            bool SelfAssigned = inObj.AssignedByUserId == UserId && inObj.UserId == UserId;
            bool HasSupervisorRole =
            CurrentUserRoles.HasFlag(AuthorizationRoles.BizAdmin) ||
            CurrentUserRoles.HasFlag(AuthorizationRoles.Service) ||
            CurrentUserRoles.HasFlag(AuthorizationRoles.Inventory) ||
            CurrentUserRoles.HasFlag(AuthorizationRoles.Sales) ||
            CurrentUserRoles.HasFlag(AuthorizationRoles.Accounting);
            if (!SelfAssigned && !HasSupervisorRole)
                AddError(ApiErrorCode.NOT_AUTHORIZED);
        }


        ////////////////////////////////////////////////////////////////////////////////////////////////
        //REPORTING
        //
        public async Task<JArray> GetReportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId)
        {
            var idList = dataListSelectedRequest.SelectedRowIds;
            JArray ReportData = new JArray();
            while (idList.Any())
            {
                var batch = idList.Take(IReportAbleObject.REPORT_DATA_BATCH_SIZE);
                idList = idList.Skip(IReportAbleObject.REPORT_DATA_BATCH_SIZE).ToArray();
                //query for this batch, comes back in db natural order unfortunately
                var batchResults = await ct.Review.AsNoTracking().Where(z => batch.Contains(z.Id)).ToArrayAsync();
                //order the results back into original
                var orderedList = from id in batch join z in batchResults on id equals z.Id select z;
                batchResults = null;
                foreach (Review w in orderedList)
                {
                    if (!ReportRenderManager.KeepGoing(jobId)) return null;
                    await PopulateVizFields(w);
                    var jo = JObject.FromObject(w);
                    if (!JsonUtil.JTokenIsNullOrEmpty(jo["CustomFields"]))
                        jo["CustomFields"] = JObject.Parse((string)jo["CustomFields"]);
                    ReportData.Add(jo);
                }
                orderedList = null;
            }
            vc.Clear();
            return ReportData;
        }
        private VizCache vc = new VizCache();

        //populate viz fields from provided object
        public async Task PopulateVizFields(Review o)
        {
            if (!vc.Has("user", o.UserId))
                vc.Add(await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync(), "user", o.UserId);
            o.UserViz = vc.Get("user", o.UserId);

            if (!vc.Has("user", o.AssignedByUserId))
                vc.Add(await ct.User.AsNoTracking().Where(x => x.Id == o.AssignedByUserId).Select(x => x.Name).FirstOrDefaultAsync(), "user", o.AssignedByUserId);
            o.AssignedByUserViz = vc.Get("user", o.AssignedByUserId);

            if (!vc.Has($"b{o.SockType}{o.ObjectId}"))
                vc.Add(BizObjectNameFetcherDirect.Name((SockType)o.SockType, (long)o.ObjectId, UserTranslationId, ct), $"b{o.SockType}{o.ObjectId}");
            o.ReviewObjectViz = vc.Get($"b{o.SockType}{o.ObjectId}");

            if (o.ReviewObjectViz.StartsWith("LT:"))
            {
                if (!vc.Has(o.ReviewObjectViz))
                    vc.Add(await Translate(o.ReviewObjectViz), o.ReviewObjectViz);
                o.ReviewObjectViz = vc.Get(o.ReviewObjectViz);
            }
        }



        ////////////////////////////////////////////////////////////////////////////////////////////////       
        // IMPORT EXPORT
        //

        public async Task<JArray> GetExportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId)
        {
            //for now just re-use the report data code
            //this may turn out to be the pattern for most biz object types but keeping it seperate allows for custom usage from time to time
            return await GetReportData(dataListSelectedRequest, jobId);
        }

        public async Task<List<string>> ImportData(AyImportData importData)
        {
            List<string> ImportResult = new List<string>();
            string ImportTag = $"imported-{FileUtil.GetSafeDateFileName()}";

            var jsset = JsonSerializer.CreateDefault(new JsonSerializerSettings { ContractResolver = new Sockeye.Util.JsonUtil.ShouldSerializeContractResolver(new string[] { "Concurrency", "Id", "CustomFields" }) });
            foreach (JObject j in importData.Data)
            {
                var w = j.ToObject<Review>(jsset);
                if (j["CustomFields"] != null)
                    w.CustomFields = j["CustomFields"].ToString();
                w.Tags.Add(ImportTag);//so user can find them all and revert later if necessary
                var res = await CreateAsync(w);
                if (res == null)
                {
                    ImportResult.Add($"* {w.Name} - {this.GetErrorsAsString()}");
                    this.ClearErrors();
                }
                else
                {
                    ImportResult.Add($"{w.Name} - ok");
                }
            }
            return ImportResult;
        }




        ////////////////////////////////////////////////////////////////////////////////////////////////
        //JOB / OPERATIONS
        //
        public async Task HandleJobAsync(OpsJob job)
        {
            //Hand off the particular job to the corresponding processing code
            //NOTE: If this code throws an exception the caller (JobsBiz::ProcessJobsAsync) will automatically set the job to failed and log the exeption so 
            //basically any error condition during job processing should throw up an exception if it can't be handled
            switch (job.JobType)
            {
                case JobType.BatchCoreObjectOperation:
                    await ProcessBatchJobAsync(job);
                    break;
                default:
                    throw new System.ArgumentOutOfRangeException($"ReviewBiz.HandleJob-> Invalid job type{job.JobType.ToString()}");
            }
        }



        private async Task ProcessBatchJobAsync(OpsJob job)
        {
            await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Running);
            await JobsBiz.LogJobAsync(job.GId, $"LT:StartJob {job.SubType}");
            List<long> idList = new List<long>();
            long FailedObjectCount = 0;
            JObject jobData = JObject.Parse(job.JobInfo);
            if (jobData.ContainsKey("idList"))
                idList = ((JArray)jobData["idList"]).ToObject<List<long>>();
            else
                idList = await ct.Review.AsNoTracking().Select(z => z.Id).ToListAsync();
            bool SaveIt = false;

            //---------------------------------
            //case 4192
            TimeSpan ProgressAndCancelCheckSpan = new TimeSpan(0, 0, ServerBootConfig.JOB_PROGRESS_UPDATE_AND_CANCEL_CHECK_SECONDS);
            DateTime LastProgressCheck = DateTime.UtcNow.Subtract(new TimeSpan(1, 1, 1, 1, 1));
            var TotalRecords = idList.LongCount();
            long CurrentRecord = -1;
            //---------------------------------
            foreach (long id in idList)
            {
                try
                {
                    //--------------------------------
                    //case 4192
                    //Update progress / cancel requested?                    
                    CurrentRecord++;
                    if (DateUtil.IsAfterDuration(LastProgressCheck, ProgressAndCancelCheckSpan))
                    {
                        await JobsBiz.UpdateJobProgressAsync(job.GId, $"{CurrentRecord}/{TotalRecords}");
                        if (await JobsBiz.GetJobStatusAsync(job.GId) == JobStatus.CancelRequested)
                            break;
                        LastProgressCheck = DateTime.UtcNow;
                    }
                    //---------------------------------
                    SaveIt = false;
                    ClearErrors();
                    Review o = null;
                    //save a fetch if it's a delete
                    if (job.SubType != JobSubType.Delete)
                        o = await GetAsync(id, false);
                    switch (job.SubType)
                    {
                        case JobSubType.TagAddAny:
                        case JobSubType.TagAdd:
                        case JobSubType.TagRemoveAny:
                        case JobSubType.TagRemove:
                        case JobSubType.TagReplaceAny:
                        case JobSubType.TagReplace:
                            SaveIt = TagBiz.ProcessBatchTagOperation(o.Tags, (string)jobData["tag"], jobData.ContainsKey("toTag") ? (string)jobData["toTag"] : null, job.SubType);
                            break;
                        case JobSubType.Delete:
                            if (!await DeleteAsync(id))
                            {
                                await JobsBiz.LogJobAsync(job.GId, $"LT:Errors {GetErrorsAsString()} id {id}");
                                FailedObjectCount++;
                            }
                            break;
                        default:
                            throw new System.ArgumentOutOfRangeException($"ProcessBatchJobAsync -> Invalid job Subtype{job.SubType}");
                    }
                    if (SaveIt)
                    {
                        o = await PutAsync(o);
                        if (o == null)
                        {
                            await JobsBiz.LogJobAsync(job.GId, $"LT:Errors {GetErrorsAsString()} id {id}");
                            FailedObjectCount++;
                        }
                    }

                    //delay so we're not tying up all the resources in a tight loop
                    await Task.Delay(Sockeye.Util.ServerBootConfig.JOB_OBJECT_HANDLE_BATCH_JOB_LOOP_DELAY);
                }
                catch (Exception ex)
                {
                    await JobsBiz.LogJobAsync(job.GId, $"LT:Errors id({id})");
                    await JobsBiz.LogJobAsync(job.GId, ExceptionUtil.ExtractAllExceptionMessages(ex));
                }
            }

            //---------------------------------
            //case 4192
            await JobsBiz.UpdateJobProgressAsync(job.GId, $"{++CurrentRecord}/{TotalRecords}");
            //---------------------------------
            await JobsBiz.LogJobAsync(job.GId, $"LT:BatchJob {job.SubType} {idList.Count}{(FailedObjectCount > 0 ? " - LT:Failed " + FailedObjectCount : "")}");
            await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Completed);
        }



        ////////////////////////////////////////////////////////////////////////////////////////////////
        // NOTIFICATION PROCESSING
        //
        public async Task HandlePotentialNotificationEvent(SockEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
        {
            if (ServerBootConfig.MIGRATING) return;
            ILogger log = Sockeye.Util.ApplicationLogging.CreateLogger<ReviewBiz>();
            
            log.LogDebug($"HandlePotentialNotificationEvent processing:  [SockType:{this.BizType}, AyaEvent:{ayaEvent}]");

            bool isNew = currentObj == null;

            Review o = (Review)proposedObj;

            //STANDARD EVENTS FOR ALL OBJECTS
            await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);

            //SPECIFIC EVENTS FOR THIS OBJECT
            #region OLD

            //OLD general notification code removed in favor of specific event for review imminent
            // //CREATED / MODIFIED
            // if (ayaEvent == AyaEvent.Created || ayaEvent == AyaEvent.Modified)
            // {
            //     //OVERDUE pseudo event
            //     {
            //         //Remove prior 
            //         await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.SockType, proposedObj.Id, NotifyEventType.GeneralNotification);//assumes only general event for this object type is overdue here

            //         //set a deadman automatic internal notification if goes past due
            //         var r = (Review)proposedObj;

            //         //it not completed yet and not overdue already (which could indicate an import or something)
            //         if (r.CompletedDate == null && r.ReviewDate > DateTime.UtcNow)
            //         {
            //             //Notify user
            //             await NotifyEventHelper.EnsureDefaultInAppUserNotificationSubscriptionExists(r.UserId, ct);
            //             {
            //                 var gensubs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.GeneralNotification && z.UserId == r.UserId).ToListAsync();
            //                 foreach (var sub in gensubs)
            //                 {
            //                     var eventNameTranslated = await TranslationBiz.GetTranslationForUserStaticAsync("ReviewOverDue", r.UserId);
            //                     NotifyEvent n = new NotifyEvent()
            //                     {
            //                         EventType = NotifyEventType.GeneralNotification,
            //                         UserId = r.UserId,
            //                         ObjectId = proposedObj.Id,
            //                         SockType = SockType.Review,
            //                         NotifySubscriptionId = sub.Id,
            //                         Name = $"{eventNameTranslated} - {proposedObj.Name}",
            //                         EventDate = r.ReviewDate
            //                     };
            //                     await ct.NotifyEvent.AddAsync(n);
            //                     log.LogDebug($"Adding NotifyEvent:  [{n.ToString()}]");
            //                 }
            //                 if (gensubs.Count > 0)
            //                     await ct.SaveChangesAsync();
            //             }

            //             //Notify supervisor
            //             if (r.UserId != r.AssignedByUserId)
            //             {
            //                 await NotifyEventHelper.EnsureDefaultInAppUserNotificationSubscriptionExists(r.AssignedByUserId, ct);
            //                 var gensubs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.GeneralNotification && z.UserId == r.AssignedByUserId).ToListAsync();
            //                 foreach (var sub in gensubs)
            //                 {
            //                     var eventNameTranslated = await TranslationBiz.GetTranslationForUserStaticAsync("ReviewOverDue", r.AssignedByUserId);
            //                     NotifyEvent n = new NotifyEvent()
            //                     {
            //                         EventType = NotifyEventType.GeneralNotification,
            //                         UserId = r.AssignedByUserId,
            //                         ObjectId = proposedObj.Id,
            //                         SockType = SockType.Review,
            //                         NotifySubscriptionId = sub.Id,
            //                         Name = $"{eventNameTranslated} - {proposedObj.Name}",
            //                         EventDate = r.ReviewDate
            //                     };
            //                     await ct.NotifyEvent.AddAsync(n);
            //                     log.LogDebug($"Adding NotifyEvent:  [{n.ToString()}]");
            //                 }
            //                 if (gensubs.Count > 0)
            //                     await ct.SaveChangesAsync();
            //             }
            //         }
            //     }//overdue event
            // }//custom events for created / modified
            #endregion old

            //## DELETED EVENTS

            //any event added below needs to be removed, so
            //just blanket remove any event for this object of eventtype that would be added below here
            //do it regardless any time there's an update and then
            //let this code below handle the refreshing addition that could have changes
            await NotifyEventHelper.ClearPriorEventsForObject(ct, SockType.Review, o.Id, NotifyEventType.ReviewImminent);

            //## CREATED / MODIFIED EVENTS
            if (ayaEvent == SockEvent.Created || ayaEvent == SockEvent.Modified)
            {
                //# REVIEW IMMINENT
                if (o.CompletedDate == null && o.ReviewDate > DateTime.UtcNow)
                {
                    //notify users  (time delayed)
                    var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.ReviewImminent).ToListAsync();
                    foreach (var sub in subs)
                    {
                        //not for inactive users
                        if (!await UserBiz.UserIsActive(sub.UserId)) continue;

                        //Tag match? (will be true if no sub tags so always safe to call this)
                        if (NotifyEventHelper.ObjectHasAllSubscriptionTags(o.Tags, sub.Tags))
                        {

                            NotifyEvent n = new NotifyEvent()
                            {
                                EventType = NotifyEventType.ReviewImminent,
                                UserId = sub.UserId,
                                SockType = o.SockType,
                                ObjectId = o.Id,
                                NotifySubscriptionId = sub.Id,
                                Name = o.Name,
                                EventDate = o.ReviewDate
                            };
                            await ct.NotifyEvent.AddAsync(n);
                            log.LogDebug($"Adding NotifyEvent:  [{n.ToString()}]");
                            await ct.SaveChangesAsync();
                        }
                    }

                }//review imminent

            }//end of process notifications

        }//end of process notifications





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

    }//eoc


}//eons

