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

namespace AyaNova.Biz
{


    internal class UserBiz : BizObject, IJobObject, ISearchAbleObject
    {

        public bool SeedOrImportRelaxedRulesMode { get; set; }

        internal UserBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles userRoles)
        {
            ct = dbcontext;
            UserId = currentUserId;
            UserTranslationId = userTranslationId;
            CurrentUserRoles = userRoles;
            BizType = AyaType.User;
            SeedOrImportRelaxedRulesMode = false;//default
        }

        //This is where active tech license consumers are accounted for
        internal static async Task<long> ActiveCountAsync()
        {
            using (AyContext ct = ServiceProviderProvider.DBContext)
            {
                // var ret = await ct.Database.ExecuteSqlRawAsync($"SELECT COUNT(*) FROM auser AS a WHERE (a.active = TRUE) AND ((a.usertype = 2) OR (a.usertype = 7))");

                var ret = await ct.User.AsNoTracking().Where(z => z.Active == true && (
                    z.UserType == UserType.Service ||
                    z.UserType == UserType.ServiceContractor)).LongCountAsync();
                return ret;
            }
        }

        //Called by license processor when use downgrades to lesser amount of techs
        internal static async Task DeActivateExcessiveTechs(long KeepThisManyActiveTechs, ILogger _log)
        {
            var TotalActiveTechs = await ActiveCountAsync();
            int CountOfTechsToSetInactive = (int)(TotalActiveTechs - KeepThisManyActiveTechs);
            if (CountOfTechsToSetInactive < 1) return;
            _log.LogInformation($"New license is a downgrade with fewer scheduleable resources, deactivating {CountOfTechsToSetInactive} scheduleable users automatically so the license can be installed");
            //bugbug: exactly wrong, deactivating the most recently logged in over some who never logged in, could be that null login is causing it?
            using (AyContext ct = ServiceProviderProvider.DBContext)
            {

                //Algorithm: For deactivation favor subcontractors first over servicetechs, favor ones that have no login records and finally favor by oldest last login
                //theory is to catch the least likely to be currently active techs          

                var NoLoginTechList = await ct.User.Where(z => z.Active == true && z.LastLogin == null && (
                                z.UserType == UserType.Service ||
                                z.UserType == UserType.ServiceContractor)).OrderByDescending(z => z.UserType).Take(CountOfTechsToSetInactive).ToListAsync();

                CountOfTechsToSetInactive -= NoLoginTechList.Count();
                foreach (User u in NoLoginTechList)
                {
                    u.Active = false;
                    var msg = $"User {u.Name} with no prior login automatically set to inactive to free up downgraded license";
                    await NotifyEventProcessor.AddOpsProblemEvent(msg);
                    _log.LogInformation(msg);
                }
                await ct.SaveChangesAsync();


                var HasLoginTechList = await ct.User.Where(z => z.Active == true && z.LastLogin != null && (
                    z.UserType == UserType.Service ||
                    z.UserType == UserType.ServiceContractor)).OrderByDescending(z => z.UserType).ThenBy(z => z.LastLogin).Take(CountOfTechsToSetInactive).ToListAsync();
                foreach (User u in HasLoginTechList)
                {
                    u.Active = false;
                    _log.LogInformation($"User {u.Name} who last logged in {u.LastLogin} automatically set to inactive to free up license");
                }
                await ct.SaveChangesAsync();
            }
        }

        internal static void ResetSuperUserPassword()
        {
            using (AyContext ct = ServiceProviderProvider.DBContext)
            {
                User dbObject = ct.User.FirstOrDefault(z => z.Id == 1);
                dbObject.Password = Hasher.hash(dbObject.Salt, ServerBootConfig.AYANOVA_SET_SUPERUSER_PW);
                ct.SaveChanges();

                NotifyEventProcessor.AddOpsProblemEvent("AYANOVA_SET_SUPERUSER_PW setting was used at most recent server boot to reset SuperUser account password").Wait();
            }
        }


        internal static UserBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
        {
            if (httpContext != null)
                return new UserBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
            else//when called internally for internal ops there will be no context so need to set default values for that
                return new UserBiz(ct, 1, ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdminFull);
        }

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

        ////////////////////////////////////////////////////////////////////////////////////////////////
        //CREATE        
        internal async Task<dtUser> CreateAsync(User newObject)
        {
            //password and login are optional but in the sense that they can be left out in a PUT
            // but if left out here we need to generate a random value instead so they can't login but the code is happy
            //because a login name and password are required always 
            if (string.IsNullOrWhiteSpace(newObject.Password))
            {
                newObject.Password = Hasher.GenerateSalt();//set it to some big random value
            }

            if (string.IsNullOrWhiteSpace(newObject.Login))
            {
                newObject.Login = Hasher.GenerateSalt();//set it to some big random value
            }

            //This is a new user so it will have been posted with a password in plaintext which needs to be salted and hashed
            newObject.Salt = Hasher.GenerateSalt();
            newObject.Password = Hasher.hash(newObject.Salt, newObject.Password);


            newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
            newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);

            //Seeder sets user options in advance so no need to create them here in that case
            if (newObject.UserOptions == null)
            {
                newObject.UserOptions = new UserOptions();
                //todo: for now defaulting to server boot config but might need to add this to the route as an option
                //now that it's not in the actual user record itself anymore as it's kind of critical
                //revisit when get to client ui
                newObject.UserOptions.TranslationId = ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID;
            }

            await ValidateAsync(newObject, null);
            if (HasErrors)
                return null;
            else
            {

                await ct.User.AddAsync(newObject);
                await ct.SaveChangesAsync();
                await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, AyaEvent.Created), ct);
                await SearchIndexAsync(newObject, true);
                await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
                await NotifyEventProcessor.HandlePotentialNotificationEvent(AyaEvent.Created, newObject);
                dtUser retUser = new dtUser();
                CopyObject.Copy(newObject, retUser);
                return retUser;
            }
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////
        //DUPLICATE
        //
        internal async Task<User> DuplicateAsync(long id)
        {
            User dbObject = await ct.User.SingleOrDefaultAsync(z => z.Id == id);

            if (dbObject == null)
            {
                AddError(ApiErrorCode.NOT_FOUND, "id");
                return null;
            }
            User newObject = new User();
            CopyObject.Copy(dbObject, newObject, "Id, Salt, Login, Password, CurrentAuthToken, DlKey, DlKeyExpire, Wiki, Serial");
            string newUniqueName = string.Empty;
            bool NotUnique = true;
            long l = 1;
            do
            {
                newUniqueName = Util.StringUtil.UniqueNameBuilder(dbObject.Name, l++, 255);
                NotUnique = await ct.User.AnyAsync(z => z.Name == newUniqueName);
            } while (NotUnique);
            newObject.Name = newUniqueName;
            newObject.Id = 0;
            newObject.Concurrency = 0;
            newObject.Salt = Hasher.GenerateSalt();
            newObject.Password = Hasher.GenerateSalt();
            newObject.Login = Hasher.GenerateSalt();
            await ct.User.AddAsync(newObject);
            await ct.SaveChangesAsync();
            await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, AyaEvent.Created), ct);
            await SearchIndexAsync(newObject, true);
            await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
            await NotifyEventProcessor.HandlePotentialNotificationEvent(AyaEvent.Created, newObject);
            return newObject;
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////
        /// GET

        //Get one
        internal async Task<dtUser> GetAsync(long Id, bool logTheGetEvent = true)
        {

            //This is simple so nothing more here, but often will be copying to a different output object or some other ops
            var dbFullUser = await ct.User.SingleOrDefaultAsync(z => z.Id == Id);
            if (dbFullUser != null)
            {
                //Log
                if (logTheGetEvent)
                    await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, Id, BizType, AyaEvent.Retrieved), ct);


                dtUser retUser = new dtUser();
                CopyObject.Copy(dbFullUser, retUser);
                return retUser;
            }
            else return null;
        }



        ////////////////////////////////////////////////////////////////////////////////////////////////
        //UPDATE
        //
        internal async Task<User> PutAsync(User putObject)
        {
            User dbObject = await ct.User.SingleOrDefaultAsync(z => z.Id == putObject.Id);
            if (dbObject == null)
            {
                AddError(ApiErrorCode.NOT_FOUND, "id");
                return null;
            }
            User SnapshotOfOriginalDBObj = new User();
            CopyObject.Copy(dbObject, SnapshotOfOriginalDBObj);
            CopyObject.Copy(putObject, dbObject, "Id, Salt, CurrentAuthToken, LoginKey, DlKey, DlKeyExpire");
            dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags);
            dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields);

            //NOTE: It's valid to call this without intending to change login or password (null values)
            //Is the user updating the password?
            if (!string.IsNullOrWhiteSpace(putObject.Password))
            {
                //YES password is being updated:                
                dbObject.Password = Hasher.hash(SnapshotOfOriginalDBObj.Salt, putObject.Password);
            }
            else
            {
                //No, use the snapshot password value
                dbObject.Password = SnapshotOfOriginalDBObj.Password;
                dbObject.Salt = SnapshotOfOriginalDBObj.Salt;
            }
            //Updating login?
            if (!string.IsNullOrWhiteSpace(putObject.Login))
            {
                //YES Login is being updated:                
                dbObject.Login = putObject.Login;
            }
            else
            {
                //No, use the original value
                dbObject.Login = SnapshotOfOriginalDBObj.Login;
            }


            ct.Entry(dbObject).OriginalValues["Concurrency"] = putObject.Concurrency;
            await ValidateAsync(dbObject, SnapshotOfOriginalDBObj);
            if (HasErrors) return null;
            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, AyaEvent.Modified), ct);
            await SearchIndexAsync(dbObject, false);
            await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags);
            await NotifyEventProcessor.HandlePotentialNotificationEvent(AyaEvent.Modified, dbObject, SnapshotOfOriginalDBObj);


            return dbObject;
        }


        /////////////////////////////////////////////
        //PASSWORD
        //
        internal async Task<bool> ChangePasswordAsync(long userId, string newPassword)
        {
            User dbObject = await ct.User.FirstOrDefaultAsync(z => z.Id == userId);
            dbObject.Password = Hasher.hash(dbObject.Salt, newPassword);
            await ct.SaveChangesAsync();

            //Log modification and save context
            await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, BizType, AyaEvent.Modified), ct);

            return true;
        }


        private async Task SearchIndexAsync(User obj, bool isNew)
        {
            //SEARCH INDEXING
            var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, BizType);
            SearchParams.AddText(obj.Notes).AddText(obj.Name).AddText(obj.EmployeeNumber).AddText(obj.Tags).AddText(obj.Wiki).AddCustomFields(obj.CustomFields);

            if (isNew)
                await Search.ProcessNewObjectKeywordsAsync(SearchParams);
            else
                await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams);
        }

        public async Task<Search.SearchIndexProcessObjectParameters> GetSearchResultSummary(long id)
        {
            var obj = await ct.User.SingleOrDefaultAsync(z => z.Id == id);
            var SearchParams = new Search.SearchIndexProcessObjectParameters();
            if (obj != null)
                SearchParams.AddText(obj.Notes).AddText(obj.Name).AddText(obj.EmployeeNumber).AddText(obj.Tags).AddText(obj.Wiki).AddCustomFields(obj.CustomFields);

            return SearchParams;
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////
        //DELETE
        //
        internal async Task<bool> DeleteAsync(long id)
        {


            using (var transaction = await ct.Database.BeginTransactionAsync())
            {
                try
                {
                    User dbObject = await ct.User.SingleOrDefaultAsync(z => z.Id == id);
                    await ValidateCanDelete(dbObject);
                    if (HasErrors)
                        return false;
                    //Remove the object
                    ct.User.Remove(dbObject);
                    await ct.SaveChangesAsync();
                    //Delete sibling objects
                    //USEROPTIONS
                    await ct.Database.ExecuteSqlInterpolatedAsync($"delete from auseroptions where userid = {dbObject.Id}");
                    //NOTIFY SUBSCRIPTIONS
                    await ct.Database.ExecuteSqlInterpolatedAsync($"delete from anotifysubscription where userid = {dbObject.Id}");
                    //personal datalistview
                    await ct.Database.ExecuteSqlInterpolatedAsync($"delete from adatalistview where public = {false} and userid = {dbObject.Id}");

                    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);
                    await transaction.CommitAsync();
                    await NotifyEventProcessor.HandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
                }
                catch
                {
                    //Just re-throw for now, let exception handler deal, but in future may want to deal with this more here
                    throw;
                }
                return true;
            }
        }


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

        //Can save or update?
        private async Task ValidateAsync(User proposedObj, User currentObj)
        {
            //run validation and biz rules
            bool isNew = currentObj == null;



            //do we need to check the license situation?
            if (proposedObj.IsTech && proposedObj.Active)
            {
                //Yes, it might be affected depending on things
                long CurrentActiveCount = await UserBiz.ActiveCountAsync();
                long LicensedUserCount = AyaNova.Core.License.ActiveKey.ActiveNumber;

                if (isNew)
                {
                    //This operation is about to consume one more license, check that we are not at the limit already
                    CheckActiveForValidation(CurrentActiveCount, LicensedUserCount);
                }
                else
                {
                    //did anything that might affect licensing change?
                    if (!currentObj.IsTech || (!currentObj.Active))//currently not a tech or if it is it's not active
                    {
                        //going from non tech to tech and active
                        //Yes, this is about to consume one more license, check that we are not at the limit already
                        CheckActiveForValidation(CurrentActiveCount, LicensedUserCount);
                    }
                }
            }


            // TODO: check user count if not new to see if affected that way 
            //also check user count in general to see if it's exceeded
            //And maybe check it in login as well as a good central spot or wherever makes sense

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

            //Name must be less than 255 characters
            if (proposedObj.Name.Length > 255)
                AddError(ApiErrorCode.VALIDATION_LENGTH_EXCEEDED, "Name", "255 max");

            //If name is otherwise OK, check that name is unique
            if (!PropertyHasErrors("Name"))
            {
                //Use Any command is efficient way to check existance, it doesn't return the record, just a true or false
                if (await ct.User.AnyAsync(z => z.Name == proposedObj.Name && z.Id != proposedObj.Id))
                {
                    AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "Name");
                }
            }

            //LOGIN must be unique
            if (await ct.User.AnyAsync(z => z.Login == proposedObj.Login && z.Id != proposedObj.Id))
            {
                AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "Login");
            }

            //TODO: Validation rules that require future other objects that aren't present yet:
            /*
            //Don't allow to go from non scheduleable if there are any scheduled workorder items because
            //it would damage the history
            BrokenRules.Assert("UserType","User.Label.MustBeScheduleable","UserType",(mUserType==UserTypes.Schedulable) && (ScheduledUserCount(this.mID,false)>0)); 

            mUserType = value;

            BrokenRules.Assert("UserTypeInvalid","Error.Object.FieldValueNotBetween,User.Label.UserType,1,7","UserType",((int)value<1 || (int)value>6));

            bool bOutOfLicenses=OutOfLicenses;
            BrokenRules.Assert("ActiveLicense","Error.Security.UserCapacity","Active",bOutOfLicenses);
            BrokenRules.Assert("UserTypeLicense","Error.Security.UserCapacity","UserType",bOutOfLicenses);

            //Case 850
            BrokenRules.Assert("ClientIDInvalid", "Error.Object.RequiredFieldEmpty,O.Client", "ClientID", 
                (mUserType== UserTypes.Client && mClientID==Guid.Empty));

            BrokenRules.Assert("HeadOfficeIDInvalid", "Error.Object.RequiredFieldEmpty,O.HeadOffice", "HeadOfficeID",
                (mUserType == UserTypes.HeadOffice && mHeadOfficeID == Guid.Empty));


            ACTIVE: need to check user count against license when re-activating a user
                    need to check open workorders and any other critical items when de-activating a user
             */

            if (!proposedObj.UserType.IsValid())
            {
                AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "UserType");
            }

            //Validate customer type user
            if (!SeedOrImportRelaxedRulesMode && proposedObj.UserType == UserType.Customer)
            {
                if (proposedObj.CustomerId == null || proposedObj.CustomerId == 0)
                {
                    AddError(ApiErrorCode.VALIDATION_REQUIRED, "CustomerId");
                }
                else
                {
                    //verify chosen customer exists
                    if (!await ct.Customer.AnyAsync(z => z.Id == proposedObj.CustomerId))
                        AddError(ApiErrorCode.NOT_FOUND, "CustomerId");
                }
            }

            //Validate headoffice type user
            if (!SeedOrImportRelaxedRulesMode && proposedObj.UserType == UserType.HeadOffice)
            {
                if (proposedObj.HeadOfficeId == null || proposedObj.HeadOfficeId == 0)
                {
                    AddError(ApiErrorCode.VALIDATION_REQUIRED, "HeadOfficeId");
                }
                else
                {
                    //verify chosen HO exists
                    if (!await ct.HeadOffice.AnyAsync(z => z.Id == proposedObj.HeadOfficeId))
                        AddError(ApiErrorCode.NOT_FOUND, "HeadOfficeId");
                }
            }

            //Validate subcontractor type user
            if (!SeedOrImportRelaxedRulesMode && proposedObj.UserType == UserType.ServiceContractor)
            {
                if (proposedObj.SubVendorId == null || proposedObj.SubVendorId == 0)
                {
                    AddError(ApiErrorCode.VALIDATION_REQUIRED, "SubVendorId");
                }
                else
                {
                    //verify that VENDOR SubVendorId exists
                    if (!await ct.Vendor.AnyAsync(z => z.Id == proposedObj.SubVendorId))
                        AddError(ApiErrorCode.NOT_FOUND, "SubVendorId");
                }
            }

            if (!proposedObj.Roles.IsValid())
            {
                AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Roles");
            }

            //Optional employee number field must be less than 255 characters
            if (!string.IsNullOrWhiteSpace(proposedObj.EmployeeNumber) && proposedObj.EmployeeNumber.Length > 255)
                AddError(ApiErrorCode.VALIDATION_LENGTH_EXCEEDED, "EmployeeNumber", "255 max");

            //Any form customizations to validate?
            var FormCustomization = await ct.FormCustom.SingleOrDefaultAsync(z => z.FormKey == AyaType.User.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);
            }

            return;
        }

        private void CheckActiveForValidation(long CurrentActiveCount, long LicensedUserCount)
        {
            if (CurrentActiveCount >= LicensedUserCount)
            {
                AddError(ApiErrorCode.INVALID_OPERATION, null, "LT:ErrorSecurityUserCapacity");
            }
        }


        //Can delete?
        private async Task ValidateCanDelete(User inObj)
        {


            //To make this simple and avoid a whole host of issues and work
            //I've decided that a user can't be deleted if they have *any* activity in the event log
            //this way a newly created user can be deleted before they do any real work still to cover a scenario where a user
            //makes a user but then doesn't need it or did it wrong
            //This avoids the whole issues related to having to check every table everywhere for their work and 
            //the associated fuckery with trying to back them out of those tables without knock-on effects
            //They can always make any user inactive to get rid of them and it will mean referential integrity issues are not there

            //There's only one rule - have they done anything eventlog worthy yet?
            //if (await ct.Event.Select(z => z).Where(z => z.UserId == inObj.Id).Count() > 0)
            if (await ct.Event.AnyAsync(z => z.UserId == inObj.Id))
            {
                AddError(ApiErrorCode.INVALID_OPERATION, null, "LT:ErrorDBForeignKeyViolation");
                return;
            }

        }


        ////////////////////////////////////////////////////////////////////////////////////////////////
        // Utilities
        //
        internal static async Task<User> ValidateDownloadTokenAndReturnUserAsync(string dlToken, AyContext ct)
        {
            if (string.IsNullOrWhiteSpace(dlToken))
                return null;
            //get user by key, if not found then reject
            var DownloadUser = await ct.User.AsNoTracking().SingleOrDefaultAsync(z => z.DlKey == dlToken && z.Active == true);
            if (DownloadUser == null)
                return null;
            //this is necessary because they might have an expired JWT but this would just keep on working without a date check
            //the default is the same timespan as the jwt so it's all good
            var utcNow = new DateTimeOffset(DateTime.Now.ToUniversalTime(), TimeSpan.Zero);
            if (DownloadUser.DlKeyExpire < utcNow.DateTime)
                return null;
            return DownloadUser;
        }

        //replaced by dtUser object instead
        // internal static object CleanUserForReturn(User o)
        // {
        //     return new
        //     {
        //         Id = o.Id,
        //         Concurrency = o.Concurrency,
        //         Active = o.Active,
        //         Name = o.Name,
        //         Roles = o.Roles,
        //         TranslationId = o.UserOptions.TranslationId,
        //         UserType = o.UserType,
        //         EmployeeNumber = o.EmployeeNumber,
        //         Notes = o.Notes,
        //         CustomerId = o.CustomerId,
        //         HeadOfficeId = o.HeadOfficeId,
        //         SubVendorId = o.SubVendorId
        //     };
        // }



        ////////////////////////////////////////////////////////////////////////////////////////////////
        // JOB / OPERATIONS
        //
        public async Task HandleJobAsync(OpsJob job)
        {
            switch (job.JobType)
            {
                case JobType.BulkCoreBizObjectOperation:
                    await ProcessBulkJobAsync(job);
                    break;
                default:
                    throw new System.ArgumentOutOfRangeException($"User.HandleJob-> Invalid job type{job.JobType.ToString()}");
            }

        }




        private async Task ProcessBulkJobAsync(OpsJob job)
        {
            await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Running);
            await JobsBiz.LogJobAsync(job.GId, $"Bulk job {job.SubType} started...");
            List<long> idList = new List<long>();
            long ChangedObjectCount = 0;
            JObject jobData = JObject.Parse(job.JobInfo);
            if (jobData.ContainsKey("idList"))
                idList = ((JArray)jobData["idList"]).ToObject<List<long>>();
            else
                idList = await ct.Widget.Select(z => z.Id).ToListAsync();
            bool SaveIt = false;
            foreach (long id in idList)
            {
                try
                {
                    SaveIt = false;
                    ClearErrors();
                    var 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.ProcessBulkTagOperation(o.Tags, (string)jobData["tag"], jobData.ContainsKey("toTag") ? (string)jobData["toTag"] : null, job.SubType);
                            break;
                        default:
                            throw new System.ArgumentOutOfRangeException($"ProcessBulkJob -> Invalid job Subtype{job.SubType}");
                    }

                    if (SaveIt)
                    {
                        User u = new User();
                        CopyObject.Copy(o, u, "Id, Salt, CurrentAuthToken, DlKey, DlKeyExpire");
                        u = await PutAsync(u);
                        if (u == null)
                            await JobsBiz.LogJobAsync(job.GId, $"Error processing item {id}: {GetErrorsAsString()}");
                        else
                            ChangedObjectCount++;
                    }
                    else
                        ChangedObjectCount++;
                }
                catch (Exception ex)
                {
                    await JobsBiz.LogJobAsync(job.GId, $"Error processing item {id}");
                    await JobsBiz.LogJobAsync(job.GId, ExceptionUtil.ExtractAllExceptionMessages(ex));
                }
            }
            await JobsBiz.LogJobAsync(job.GId, $"Bulk job {job.SubType} changed {ChangedObjectCount} of {idList.Count}");
            await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Completed);
        }



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

    }//eoc


}//eons

