using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.JsonPatch;
using EnumsNET;
using AyaNova.Util;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Biz;
using AyaNova.Models;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;

namespace AyaNova.Biz
{


    internal class UserBiz : BizObject, IJobObject, IImportAyaNova7Object
    {

        public bool SeedOrImportRelaxedRulesMode { get; set; }

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

        //todo: 
        //then after that go into widget and anywhere else that there is this associated type code being called for event and search and implement everywhere, 
        //then update seeder code to use it and get back on to the main critical path again in the todo

        internal static UserBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext)
        {
            return new UserBiz(ct, UserIdFromContext.Id(httpContext.Items), UserLocaleIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
        }

        //Version for internal use
        internal static UserBiz GetBizInternal(AyContext ct)
        {
            return new UserBiz(ct, 1, ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE_ID, AuthorizationRoles.BizAdminFull);
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////
        //CREATE        
        internal async Task<User> CreateAsync(User inObj)
        {
            //This is a new user so it will have been posted with a password in plaintext which needs to be salted and hashed
            inObj.Salt = Hasher.GenerateSalt();
            inObj.Password = Hasher.hash(inObj.Salt, inObj.Password);

            Validate(inObj, null);
            if (HasErrors)
                return null;
            else
            {
                //do stuff with User
                User outObj = inObj;
                outObj.OwnerId = UserId;
                outObj.Tags = TagUtil.NormalizeTags(outObj.Tags);
                //Seeder sets user options in advance so no need to create them here in that case
                if (outObj.UserOptions == null)
                    outObj.UserOptions = new UserOptions(UserId);

                await ct.User.AddAsync(outObj);
                //save to get Id
                await ct.SaveChangesAsync();

                //Handle child and associated items

                //Log event
                EventLogProcessor.LogEventToDatabase(new Event(UserId, outObj.Id, BizType, AyaEvent.Created), ct);

                //SEARCH INDEXING
                //Search.ProcessNewObjectKeywords( UserLocaleId, outObj.Id, BizType, outObj.Name, outObj.EmployeeNumber, outObj.Notes, outObj.Name);
                var SearchParams = new Search.SearchIndexProcessObjectParameters(UserLocaleId, outObj.Id, BizType, outObj.Name);
                SearchParams.AddWord(outObj.Notes).AddWord(outObj.Name).AddWord(outObj.EmployeeNumber).AddWord(outObj.Tags);
                Search.ProcessNewObjectKeywords(SearchParams);

                return outObj;

            }
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////
        //CREATE        
        internal User Create(AyContext TempContext, User inObj)
        {
            //This is a new user so it will have been posted with a password in plaintext which needs to be salted and hashed
            inObj.Salt = Hasher.GenerateSalt();
            inObj.Password = Hasher.hash(inObj.Salt, inObj.Password);

            Validate(inObj, null);
            if (HasErrors)
                return null;
            else
            {
                //do stuff with User
                User outObj = inObj;
                outObj.OwnerId = UserId;
                outObj.Tags = TagUtil.NormalizeTags(outObj.Tags);
                //Seeder sets user options in advance so no need to create them here in that case
                if (outObj.UserOptions == null)
                    outObj.UserOptions = new UserOptions(UserId);

                TempContext.User.Add(outObj);
                //save to get Id
                TempContext.SaveChanges();

                //Handle child and associated items

                //Log event
                EventLogProcessor.LogEventToDatabase(new Event(UserId, outObj.Id, BizType, AyaEvent.Created), TempContext);

                //SEARCH INDEXING
                // Search.ProcessNewObjectKeywords(UserLocaleId, outObj.Id, BizType, outObj.Name, outObj.EmployeeNumber, outObj.Notes, outObj.Name);
                var SearchParams = new Search.SearchIndexProcessObjectParameters(UserLocaleId, outObj.Id, BizType, outObj.Name);
                SearchParams.AddWord(outObj.Notes).AddWord(outObj.Name).AddWord(outObj.EmployeeNumber).AddWord(outObj.Tags);
                Search.ProcessNewObjectKeywords(SearchParams);

                return outObj;

            }
        }

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

        //Get one
        internal async Task<User> GetAsync(long fetchId)
        {
            //This is simple so nothing more here, but often will be copying to a different output object or some other ops
            var ret = await ct.User.SingleOrDefaultAsync(m => m.Id == fetchId);
            if (ret != null)
            {
                //Log
                EventLogProcessor.LogEventToDatabase(new Event(UserId, fetchId, BizType, AyaEvent.Retrieved), ct);
            }
            return ret;
        }



        public static FilterOptions FilterOptions(long localizeToLocaleId = 0)
        {
            //NOTE: All column names are lowercase to conform with Postgres AyaNova DB which uses lowercase for all identifiers
            //Also all list keys are lower case for consistency
            FilterOptions f = new FilterOptions("user");
            f.
            AddField("id", "ID", AyDataType.Integer).
            AddField("name", "CommonName", AyDataType.Text).
            AddField("active", "CommonActive", AyDataType.Bool).
            AddField("tags", "Tags", AyDataType.Tags).
            AddField("EmployeeNumber", "UserEmployeeNumber", AyDataType.Text).
            AddField("usertype", "UserUserType", AyDataType.Enum).
            AddField("notes", "WidgetNotes", AyDataType.Text);

            if (localizeToLocaleId != 0)
                f.Localize(localizeToLocaleId);
            return f;
        }


        //get many (paged)
        internal async Task<ApiPagedResponse<System.Object>> GetManyAsync(IUrlHelper Url, string routeName, PagingOptions pagingOptions)
        {

            pagingOptions.Offset = pagingOptions.Offset ?? PagingOptions.DefaultOffset;
            pagingOptions.Limit = pagingOptions.Limit ?? PagingOptions.DefaultLimit;

            // var items = await ct.User
            //                 .AsNoTracking()
            //                 .OrderBy(m => m.Id)
            //                 .Skip(pagingOptions.Offset.Value)
            //                 .Take(pagingOptions.Limit.Value)
            //                 .ToArrayAsync();

            // var totalRecordCount = await ct.User.CountAsync();
            // var pageLinks = new PaginationLinkBuilder(Url, routeName, null, pagingOptions, totalRecordCount).PagingLinksObject();

            //BUILD THE QUERY
            //base query
            var q = "SELECT *, xmin FROM AUSER ";

            //GET THE FILTER / SORT
            if (pagingOptions.DataFilterId > 0)
            {
                var TheFilter = await ct.DataFilter.FirstOrDefaultAsync(x => x.Id == pagingOptions.DataFilterId);

                //BUILD WHERE AND APPEND IT
                q = q + FilterSqlCriteriaBuilder.DataFilterToSQLCriteria(TheFilter, UserBiz.FilterOptions(), UserId);

                //BUILD ORDER BY AND APPEND IT
                q = q + FilterSqlOrderByBuilder.DataFilterToSQLOrderBy(TheFilter);
            }
            else
            {
                //GET DEFAULT ORDER BY
                q = q + FilterSqlOrderByBuilder.DefaultOrderBy();
            }


#pragma warning disable EF1000

            var items = await ct.User
                                        .AsNoTracking()
                                        .FromSql(q)
                                        .Skip(pagingOptions.Offset.Value)
                                        .Take(pagingOptions.Limit.Value)
                                        .ToArrayAsync();


            var totalRecordCount = await ct.User
                                                   .AsNoTracking()
                                                   .FromSql(q)
                                                   .CountAsync();
#pragma warning restore EF1000

            int itemCount = items.Count();//totalRecordCount doesn't skip and take so not usable here
            var cleanedItems = new System.Object[itemCount];
            for (int i = 0; i < itemCount; i++)
            {
                cleanedItems[i] = CleanUserForReturn(items[i]);
            }

            var pageLinks = new PaginationLinkBuilder(Url, routeName, null, pagingOptions, totalRecordCount).PagingLinksObject();
            ApiPagedResponse<System.Object> pr = new ApiPagedResponse<System.Object>(cleanedItems, pageLinks);
            return pr;
        }


        //get picklist (paged)
        internal async Task<ApiPagedResponse<NameIdItem>> GetPickListAsync(IUrlHelper Url, string routeName, PagingOptions pagingOptions, string q)
        {
            pagingOptions.Offset = pagingOptions.Offset ?? PagingOptions.DefaultOffset;
            pagingOptions.Limit = pagingOptions.Limit ?? PagingOptions.DefaultLimit;

            NameIdItem[] items;
            int totalRecordCount = 0;

            if (!string.IsNullOrWhiteSpace(q))
            {
                items = await ct.User
                                .AsNoTracking()
                                .Where(m => EF.Functions.ILike(m.Name, q))
                                .OrderBy(m => m.Name)
                                .Skip(pagingOptions.Offset.Value)
                                .Take(pagingOptions.Limit.Value)
                                 .Select(m => new NameIdItem()
                                 {
                                     Id = m.Id,
                                     Name = m.Name
                                 }).ToArrayAsync();

                totalRecordCount = await ct.User.Where(m => EF.Functions.ILike(m.Name, q)).CountAsync();
            }
            else
            {
                items = await ct.User
                            .AsNoTracking()
                            .OrderBy(m => m.Name)
                            .Skip(pagingOptions.Offset.Value)
                            .Take(pagingOptions.Limit.Value)
                            .Select(m => new NameIdItem()
                            {
                                Id = m.Id,
                                Name = m.Name
                            }).ToArrayAsync();

                totalRecordCount = await ct.User.CountAsync();
            }



            var pageLinks = new PaginationLinkBuilder(Url, routeName, null, pagingOptions, totalRecordCount).PagingLinksObject();

            ApiPagedResponse<NameIdItem> pr = new ApiPagedResponse<NameIdItem>(items, pageLinks);
            return pr;
        }




        ////////////////////////////////////////////////////////////////////////////////////////////////
        //UPDATE
        //

        //put
        internal bool Put(User dbObj, User inObj)
        {
            //preserve the owner ID if none was specified
            if (inObj.OwnerId == 0)
                inObj.OwnerId = dbObj.OwnerId;

            //Get a snapshot of the original db value object before changes
            User SnapshotObj = new User();
            CopyObject.Copy(dbObj, SnapshotObj);

            //Update the db object with the PUT object values
            CopyObject.Copy(inObj, dbObj, "Id, Salt");
            dbObj.Tags = TagUtil.NormalizeTags(dbObj.Tags);

            //Is the user updating the password?
            if (!string.IsNullOrWhiteSpace(inObj.Password) && SnapshotObj.Password != inObj.Password)
            {
                //YES password is being updated:                
                dbObj.Password = Hasher.hash(SnapshotObj.Salt, inObj.Password);
            }
            else
            {
                //No, use the snapshot password value
                dbObj.Password = SnapshotObj.Password;
                dbObj.Salt = SnapshotObj.Salt;
            }


            //Set "original" value of concurrency token to input token
            //this will allow EF to check it out
            ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = inObj.ConcurrencyToken;

            Validate(dbObj, SnapshotObj);
            if (HasErrors)
                return false;


            //Log modification
            EventLogProcessor.LogEventToDatabase(new Event(UserId, dbObj.Id, BizType, AyaEvent.Modified), ct);
            //Update keywords
            // Search.ProcessUpdatedObjectKeywords(UserLocaleId, dbObj.Id, BizType, dbObj.Name, dbObj.EmployeeNumber, dbObj.Notes, dbObj.Name);
            var SearchParams = new Search.SearchIndexProcessObjectParameters(UserLocaleId, dbObj.Id, BizType, dbObj.Name);
            SearchParams.AddWord(dbObj.Notes).AddWord(dbObj.Name).AddWord(dbObj.EmployeeNumber).AddWord(dbObj.Tags);
            Search.ProcessNewObjectKeywords(SearchParams);

            return true;
        }

        //patch
        internal bool Patch(User dbObj, JsonPatchDocument<User> objectPatch, uint concurrencyToken)
        {
            //Validate Patch is allowed
            if (!ValidateJsonPatch<User>.Validate(this, objectPatch)) return false;

            //make a snapshot of the original for validation but update the original to preserve workflow           
            User snapshotObj = new User();
            CopyObject.Copy(dbObj, snapshotObj);

            //Do the patching
            objectPatch.ApplyTo(dbObj);
            dbObj.Tags = TagUtil.NormalizeTags(dbObj.Tags);

            //Is the user patching the password?
            if (!string.IsNullOrWhiteSpace(dbObj.Password) && dbObj.Password != snapshotObj.Password)
            {
                //YES password is being updated:                
                dbObj.Password = Hasher.hash(dbObj.Salt, dbObj.Password);
            }

            ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = concurrencyToken;
            Validate(dbObj, snapshotObj);
            if (HasErrors)
                return false;

            //Log modification
            EventLogProcessor.LogEventToDatabase(new Event(UserId, dbObj.Id, BizType, AyaEvent.Modified), ct);

            //Update keywords
            // Search.ProcessUpdatedObjectKeywords(UserLocaleId, dbObj.Id, BizType, dbObj.Name, dbObj.EmployeeNumber, dbObj.Notes, dbObj.Name);
            var SearchParams = new Search.SearchIndexProcessObjectParameters(UserLocaleId, dbObj.Id, BizType, dbObj.Name);
            SearchParams.AddWord(dbObj.Notes).AddWord(dbObj.Name).AddWord(dbObj.EmployeeNumber).AddWord(dbObj.Tags);
            Search.ProcessNewObjectKeywords(SearchParams);

            return true;
        }


        ////////////////////////////////////////////////////////////////////////////////////////////////
        //DELETE
        //
        internal bool Delete(User dbObj)
        {
            ValidateCanDelete(dbObj);
            if (HasErrors)
                return false;

            //Remove the object
            ct.User.Remove(dbObj);
            ct.SaveChanges();

            //Delete sibling objects
            //USEROPTIONS
            ct.Database.ExecuteSqlCommand($"delete from auseroptions where userid={dbObj.Id}");

            //Event log process delete
            EventLogProcessor.DeleteObject(UserId, BizType, dbObj.Id, dbObj.Name, ct);
            ct.SaveChanges();

            //Delete search index
            Search.ProcessDeletedObjectKeywords(dbObj.Id, BizType);

            // //TAGS
            // TagMapBiz.DeleteAllForObject(new AyaTypeId(BizType, dbObj.Id), ct);
            // ct.SaveChanges();

            return true;
        }


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

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


            if (isNew) //Yes, no currentObj
            {

            }

            //OwnerId required
            if (!isNew)
            {
                if (proposedObj.OwnerId == 0)
                    AddError(ValidationErrorType.RequiredPropertyEmpty, "OwnerId");
            }

            //Name required
            if (string.IsNullOrWhiteSpace(proposedObj.Name))
                AddError(ValidationErrorType.RequiredPropertyEmpty, "Name");

            //Name must be less than 255 characters
            if (proposedObj.Name.Length > 255)
                AddError(ValidationErrorType.LengthExceeded, "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 (ct.User.Any(m => m.Name == proposedObj.Name && m.Id != proposedObj.Id))
                {
                    AddError(ValidationErrorType.NotUnique, "Name");
                }
            }



            //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(ValidationErrorType.InvalidValue, "UserType");
            }

            //Validate client type user
            if (!SeedOrImportRelaxedRulesMode && proposedObj.UserType == UserType.Client)
            {
                if (proposedObj.ClientId == null || proposedObj.ClientId == 0)
                {
                    AddError(ValidationErrorType.RequiredPropertyEmpty, "ClientId");
                }
                else
                {
                    //verify that client exists
                    //TODO WHEN CLIENT OBJECT MADE if(!ct.Client.any(m))
                }
            }

            //Validate headoffice type user
            if (!SeedOrImportRelaxedRulesMode && proposedObj.UserType == UserType.HeadOffice)
            {
                if (proposedObj.HeadOfficeId == null || proposedObj.HeadOfficeId == 0)
                {
                    AddError(ValidationErrorType.RequiredPropertyEmpty, "HeadOfficeId");
                }
                else
                {
                    //verify that HeadOfficeId exists
                    //TODO WHEN HEADOFFICE OBJECT MADE if(!ct.HeadOffice.any(m))
                }
            }

            //Validate headoffice type user
            if (!SeedOrImportRelaxedRulesMode && proposedObj.UserType == UserType.Subcontractor)
            {
                if (proposedObj.SubVendorId == null || proposedObj.SubVendorId == 0)
                {
                    AddError(ValidationErrorType.RequiredPropertyEmpty, "SubVendorId");
                }
                else
                {
                    //verify that VENDOR SubVendorId exists
                    //TODO WHEN VENDOR OBJECT MADE if(!ct.Vendor.any(m))
                }
            }

            if (!proposedObj.Roles.IsValid())
            {
                AddError(ValidationErrorType.InvalidValue, "Roles");
            }

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



            return;
        }


        //Can delete?
        private void ValidateCanDelete(User inObj)
        {
            //TODO: Validate can delete a user
            //TODO: handle all the related tables that require deletion
            //whatever needs to be check to delete this object

            /* V7 code related to this for reference
            
            	#region Direct delete
			Criteria crit = (Criteria)Criteria;
			if(crit.ID==User.AdministratorID || crit.ID==User.CurrentThreadUserID)
			{
				throw new System.Security.SecurityException(
					string.Format(
					LocalizedTextTable.GetLocalizedTextDirect("Error.Security.NotAuthorizedToDelete"),
					LocalizedTextTable.GetLocalizedTextDirect("O.User")));
			}

			//CHANGE: 14-March-2006  reorganized this and added more items to delete so that a user can 
			//actually be deleted

			//Delete user and child objects
			DBCommandWrapper cmDeleteUser = DBUtil.GetCommandFromSQL("DELETE FROM aUser WHERE aID = @ID;");
			cmDeleteUser.AddInParameter("@ID",DbType.Guid,crit.ID);

			DBCommandWrapper cmDeleteUserCertificationAssigned = DBUtil.GetCommandFromSQL("DELETE FROM aUserCertificationAssigned WHERE aUserID = @ID;");
			cmDeleteUserCertificationAssigned.AddInParameter("@ID",DbType.Guid,crit.ID);

			DBCommandWrapper cmDeleteUserSkillAssigned = DBUtil.GetCommandFromSQL("DELETE FROM aUserSkillAssigned WHERE aUserID = @ID;");
			cmDeleteUserSkillAssigned.AddInParameter("@ID",DbType.Guid,crit.ID);
					
			DBCommandWrapper cmDeleteUserExplorerBarLayout = DBUtil.GetCommandFromSQL("DELETE FROM aUIExplorerBarLayout WHERE aUserID = @ID;");
			cmDeleteUserExplorerBarLayout.AddInParameter("@ID",DbType.Guid,crit.ID);
					
			DBCommandWrapper cmDeleteUserGridLayout = DBUtil.GetCommandFromSQL("DELETE FROM aUIGridLayout WHERE aUserID = @ID;");
			cmDeleteUserGridLayout.AddInParameter("@ID",DbType.Guid,crit.ID);

			DBCommandWrapper cmDeleteUserFormSetting = DBUtil.GetCommandFromSQL("DELETE FROM aUIUserFormSetting WHERE aUserID = @ID;");
			cmDeleteUserFormSetting.AddInParameter("@ID",DbType.Guid,crit.ID);

            DBCommandWrapper cmDeleteUserGridLastView = DBUtil.GetCommandFromSQL("DELETE FROM aUIUserGridLastView WHERE aUserID = @ID;");
            cmDeleteUserGridLastView.AddInParameter("@ID", DbType.Guid, crit.ID);
           
            DBCommandWrapper cmDeleteDeliveries = DBUtil.GetCommandFromSQL("DELETE FROM aNotifyDeliverySetting WHERE aUserID = @ID;");
            cmDeleteDeliveries.AddInParameter("@ID", DbType.Guid, crit.ID);
	

			using (IDbConnection connection = DBUtil.DB.GetConnection())
			{
				connection.Open();
				IDbTransaction transaction = connection.BeginTransaction();

				try
				{
                    //Added: 16-Nov-2006 to clear out notification subscriptions when user 
                    //is deleted
                    NotifySubscriptions.DeleteItems(crit.ID, transaction);

                    DBUtil.DB.ExecuteNonQuery(cmDeleteUserGridLastView, transaction);
					DBUtil.DB.ExecuteNonQuery(cmDeleteUserGridLayout, transaction);
					DBUtil.DB.ExecuteNonQuery(cmDeleteUserFormSetting, transaction);
					DBUtil.DB.ExecuteNonQuery(cmDeleteUserExplorerBarLayout, transaction);
					DBUtil.DB.ExecuteNonQuery(cmDeleteUserCertificationAssigned, transaction);
					DBUtil.DB.ExecuteNonQuery(cmDeleteUserSkillAssigned, transaction);

                    //Added:16-Nov-2006
                    DBUtil.DB.ExecuteNonQuery(cmDeleteDeliveries, transaction);


					DBUtil.DB.ExecuteNonQuery(cmDeleteUser, transaction);
					DBUtil.RemoveKeywords(transaction,RootObjectTypes.User,crit.ID);
					DBUtil.RemoveDocs(transaction,RootObjectTypes.User,crit.ID);
					
					// Commit the transaction
					transaction.Commit();
							
				}
				catch
				{
					// Rollback transaction 
					transaction.Rollback();
					throw;
				}
				finally
				{
					connection.Close();
				}
			}


			#endregion
		
             */

        }


        ////////////////////////////////////////////////////////////////////////////////////////////////
        // Utilities
        //
        internal static object CleanUserForReturn(User o)
        {
            return new
            {
                Id = o.Id,
                ConcurrencyToken = o.ConcurrencyToken,
                OwnerId = o.OwnerId,
                Active = o.Active,
                Name = o.Name,
                Roles = o.Roles,
                LocaleId = o.LocaleId,
                UserType = o.UserType,
                EmployeeNumber = o.EmployeeNumber,
                Notes = o.Notes,
                ClientId = o.ClientId,
                HeadOfficeId = o.HeadOfficeId,
                SubVendorId = o.SubVendorId
            };
        }



        ////////////////////////////////////////////////////////////////////////////////////////////////
        // JOB / OPERATIONS
        //
        public async Task HandleJobAsync(OpsJob job)
        {
            //just to hide compiler warning for now
            await Task.CompletedTask;
            //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)
            {

                default:
                    throw new System.ArgumentOutOfRangeException($"UserBiz.HandleJob-> Invalid job type{job.JobType.ToString()}");
            }

        }




        public async Task<bool> ImportV7Async(JObject j, List<ImportAyaNova7MapItem> importMap, Guid jobId, Dictionary<string, Dictionary<Guid, string>> tagLists)
        {

            //NEEDS 3 of the tag lists

            //TODO: Some of these items will need to be imported in future USEROPTIONS object that doesn't exist yet
            #region V7 record format
            /*
            {
                "DefaultLanguage": "Custom English",
                "DefaultServiceTemplateID": "ca83a7b8-4e5f-4a7b-a02b-9cf78d5f983f",
                "UserType": 2,
                "Active": true,
                "ClientID": "00000000-0000-0000-0000-000000000000",
                "HeadOfficeID": "00000000-0000-0000-0000-000000000000",
                * "MemberOfGroup": "0f8a80ff-4b03-4114-ae51-2d13b812dd65",
                "Created": "03/21/2005 07:19 AM",
                "Modified": "09/15/2015 12:22 PM",
                "Creator": "2ecc77fc-69e2-4a7e-b88d-bd0ecaf36aed",
                "Modifier": "1d859264-3f32-462a-9b0c-a67dddfdf4d3",
                "ID": "1d859264-3f32-462a-9b0c-a67dddfdf4d3",
                "FirstName": "Hank",
                "LastName": "Rearden",
                "Initials": "HR",
                "EmployeeNumber": "EMP1236",
                "PageAddress": "",
                "PageMaxText": 24,
                "Phone1": "",
                "Phone2": "",
                "EmailAddress": "",
                "UserCertifications": [
                    {
                    "Created": "12/22/2005 02:07 PM",
                    "Creator": "2ecc77fc-69e2-4a7e-b88d-bd0ecaf36aed",
                    "Modified": "12/22/2005 02:08 PM",
                    "Modifier": "2ecc77fc-69e2-4a7e-b88d-bd0ecaf36aed",
                    "ID": "4492360c-43e4-4209-9f33-30691b0808ed",
                    "UserCertificationID": "b2f26359-7c42-4218-923a-e949f3ef1f85",
                    "UserID": "1d859264-3f32-462a-9b0c-a67dddfdf4d3",
                    "ValidStartDate": "2005-10-11T00:00:00-07:00",
                    "ValidStopDate": "2006-10-11T00:00:00-07:00"
                    }
                ],
                "UserSkills": [
                    {
                    "Created": "12/22/2005 02:06 PM",
                    "Creator": "2ecc77fc-69e2-4a7e-b88d-bd0ecaf36aed",
                    "Modified": "12/22/2005 02:08 PM",
                    "Modifier": "2ecc77fc-69e2-4a7e-b88d-bd0ecaf36aed",
                    "ID": "1dc5ce96-f411-4885-856e-5bdb3ad79728",
                    "UserSkillID": "2e6f8b65-594c-4f6c-9cd6-e14a562daba8",
                    "UserID": "1d859264-3f32-462a-9b0c-a67dddfdf4d3"
                    },
                    {
                    "Created": "12/22/2005 02:06 PM",
                    "Creator": "2ecc77fc-69e2-4a7e-b88d-bd0ecaf36aed",
                    "Modified": "12/22/2005 02:08 PM",
                    "Modifier": "2ecc77fc-69e2-4a7e-b88d-bd0ecaf36aed",
                    "ID": "88e476d3-7526-45f5-a0dd-706c8053a63f",
                    "UserSkillID": "47a4ee94-b0e9-41b5-afe5-4b4f2c981877",
                    "UserID": "1d859264-3f32-462a-9b0c-a67dddfdf4d3"
                    }
                ],
                "Notes": "",
                "VendorID": "06e502c2-69ba-4e88-8efb-5b53c1687740",
                * "RegionID": "f856423a-d468-4344-b7b8-121e466738c6",
                * "DispatchZoneID": "00000000-0000-0000-0000-000000000000",
                "SubContractor": false,//This is not actually a feature in v7, you can just pick a vendorId for subcontractor but still be  of type technician
                "DefaultWarehouseID": "d45eab37-b6e6-4ad2-9163-66d7ba83a98c",
                "Custom1": "",
                "Custom2": "",
                "Custom3": "",
                "Custom4": "",
                "Custom5": "",
                "Custom6": "",
                "Custom7": "",
                "Custom8": "",
                "Custom9": "",
                "Custom0": "",
                "ScheduleBackColor": -2097216,
                "TimeZoneOffset": null
            }
             */

            #endregion v7 record format

            SeedOrImportRelaxedRulesMode = true;

            //some types need to import from more than one source hence the seemingly redundant switch statement for futureproofing
            switch (j["IMPORT_TASK"].Value<string>())
            {
                case "main":
                    {
                        #region Main import
                        var V7Id = new Guid(j["ID"].Value<string>());

                        //skip the administrator account but add it to the map for all the other import code that requires it
                        if (V7Id == new Guid("2ecc77fc-69e2-4a7e-b88d-bd0ecaf36aed"))
                        {
                            var mapItem = new ImportAyaNova7MapItem(V7Id, BizType, 1);
                            importMap.Add(mapItem);
                            return true;
                        }

                        //Copy values 
                        User i = new User();

                        //default owner id is manager account in RAVEN
                        i.OwnerId = 1;

                        i.Name = j["FirstName"].Value<string>() + " " + j["LastName"].Value<string>();
                        var Temp = j["UserType"].Value<int>();
                        i.UserType = (UserType)Temp;

                        //If there is a vendorId set then this user is actually a subcontractor in v7 so set accordingly
                        var VendorId = new Guid(j["VendorID"].Value<string>());
                        if (VendorId != Guid.Empty)
                        {
                            i.UserType = UserType.Subcontractor;
                        }

                        i.Active = false;//Ignore incoming value and set all imports to false so that there's no chance of licensing getting circumvented; users all need to be edited anyway for pw and login
                        i.EmployeeNumber = j["EmployeeNumber"].Value<string>();
                        i.Notes = j["Notes"].Value<string>();

                        //TAGS
                        var MemberOfGroupId = new Guid(j["MemberOfGroup"].Value<string>());
                        if (MemberOfGroupId != Guid.Empty)
                        {
                            string sTag = string.Empty;
                            if (tagLists["ScheduleableUserGroup"].TryGetValue(MemberOfGroupId, out sTag))
                            {
                                i.Tags.Add(sTag);
                            }
                        }

                        var RegionID = new Guid(j["RegionID"].Value<string>());
                        if (RegionID != Guid.Empty)
                        {
                            string sTag = string.Empty;
                            if (tagLists["Region"].TryGetValue(RegionID, out sTag))
                            {
                                i.Tags.Add(sTag);
                            }
                        }

                        var DispatchZoneID = new Guid(j["DispatchZoneID"].Value<string>());
                        if (DispatchZoneID != Guid.Empty)
                        {
                            string sTag = string.Empty;
                            if (tagLists["DispatchZone"].TryGetValue(DispatchZoneID, out sTag))
                            {
                                i.Tags.Add(sTag);
                            }
                        }

                        //User options
                        i.UserOptions = new UserOptions(i.OwnerId);

                        //TimeZone Offset
                        //Note: can be null value if not set so need to check for that here specially
                        var tzo = j["TimeZoneOffset"].Value<decimal?>() ?? 0;
                        i.UserOptions.TimeZoneOffset = tzo;


                        //Email address
                        i.UserOptions.EmailAddress = j["EmailAddress"].Value<string>();

                        //UI colour
                        i.UserOptions.UiColor = j["ScheduleBackColor"].Value<int>();


                        //Set unusable random login credentials
                        i.Salt = Hasher.GenerateSalt();
                        i.Login = Hasher.GenerateSalt();
                        i.Password = Hasher.hash(i.Salt, Hasher.GenerateSalt());

                        //No rights
                        i.Roles = AuthorizationRoles.NoRole;

                        //temporary locale id to satisfy db settings
                        i.LocaleId = ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE_ID;

                        User o = await CreateAsync(i);
                        if (HasErrors)
                        {
                            //If there are any validation errors, log in joblog and move on
                            JobsBiz.LogJob(jobId, $" -> import object \"{i.Name}\" source id {V7Id.ToString()} failed validation and was not imported: {GetErrorsAsString()} ", ct);

                            //This is a fundamental problem with the import as users are required for many things so bomb out entirely
                            //other things might be able to work around but this is too serious
                            throw new System.SystemException("UserBiz::ImportV7Async - FATAL ERROR, IMPORT FROM V7 CANNOT CONTINUE WITHOUT ALL USERS BEING IMPORTED, SEE JOB ERROR LOG FOR DETAILS");
                            // return false;
                        }
                        else
                        {
                            await ct.SaveChangesAsync();
                            var mapItem = new ImportAyaNova7MapItem(V7Id, BizType, o.Id);
                            importMap.Add(mapItem);


                        }
                        #endregion 
                    }
                    break;
                case "eventlog":
                    {
                        ImportAyaNova7Biz.LogEventCreatedModifiedEvents(j, importMap, BizType, ct);
                    }
                    break;
                case "locale":
                    {
                        #region set locale
                        //get the userId 
                        //----
                        var V7Id = new Guid(j["ID"].Value<string>());
                        var MapItem = importMap.Where(m => m.V7ObjectId == V7Id).FirstOrDefault();
                        if (MapItem == null)
                        {
                            throw new System.Exception("UserBiz::ImportV7Async-locale - FATAL ERROR, IMPORT FROM V7 CANNOT CONTINUE USER NOT FOUND IN IMPORTMAP");
                        }
                        var NewId = MapItem.NewObjectAyaTypeId.ObjectId;
                        User u = ct.User.Where(m => m.Id == NewId).FirstOrDefault();
                        if (u == null)
                        {
                            throw new System.Exception("UserBiz::ImportV7Async-locale - FATAL ERROR, IMPORT FROM V7 CANNOT CONTINUE USER NOT FOUND IN DATABASE");
                        }

                        //handle locale entries for users now that we have the locales created 
                        var V7Locale = j["DefaultLanguage"].Value<string>();

                        //Get new locale name
                        var NewLocaleName = string.Empty;
                        switch (V7Locale)
                        {
                            case "Français":
                                NewLocaleName = "fr";
                                break;
                            case "Español":
                                NewLocaleName = "es";
                                break;
                            case "Deutsch":
                                NewLocaleName = "de";
                                break;
                            case "English":
                                NewLocaleName = "en";
                                break;
                            default:
                                {
                                    //It's a custom locale, translate it from v7 original format to imported name format
                                    //make lower and replace spaces with dashes
                                    NewLocaleName = V7Locale.ToLowerInvariant().Replace(" ", "-");

                                    //ensure each character is a valid path character
                                    foreach (char c in System.IO.Path.GetInvalidFileNameChars())//is this kosher on linux?  Original code was windows
                                    {
                                        NewLocaleName = NewLocaleName.Replace(c, '_');
                                    }
                                }
                                break;
                        }

                        u.LocaleId = LocaleBiz.LocaleNameToIdStatic(NewLocaleName, ct);

                        ct.SaveChanges();
                        #endregion set locale
                    }
                    break;

                case "clientid":
                    {
                        var V7Id = new Guid(j["ID"].Value<string>());
                        //handle setting client id for user client login
                        //throw new System.NotImplementedException();
                    }
                    break;
                case "headofficeid":
                    {
                        var V7Id = new Guid(j["ID"].Value<string>());
                        //handle setting ho id for user headoffice login           
                        //throw new System.NotImplementedException();
                    }
                    break;
            }

            //just to hide compiler warning for now
            await Task.CompletedTask;
            //this is the equivalent of returning void for a Task signature with nothing to return
            return true;

        }



        //Other job handlers here...


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

    }//eoc


}//eons

