using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.JsonPatch;
using EnumsNET;
using AyaNova.Util;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Models;

namespace AyaNova.Biz
{

    internal class WidgetBiz : BizObject, IJobObject
    {

        internal WidgetBiz(AyContext dbcontext, long currentUserId, long userLocaleId, AuthorizationRoles UserRoles)
        {
            ct = dbcontext;
            UserId = currentUserId;
            UserLocaleId = userLocaleId;
            CurrentUserRoles = UserRoles;
            BizType = AyaType.Widget;
        }

        internal static WidgetBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
        {
            
            if (httpContext != null)
                return new WidgetBiz(ct, UserIdFromContext.Id(httpContext.Items), UserLocaleIdFromContext.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 WidgetBiz(ct, 1, ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE_ID, AuthorizationRoles.BizAdminFull);
        }

       

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

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

        internal async Task<Widget> GetAsync(long fetchId, 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 ret = await ct.Widget.SingleOrDefaultAsync(m => m.Id == fetchId);
            if (logTheGetEvent && ret != null)
            {
                //Log
                await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, fetchId, BizType, AyaEvent.Retrieved), ct);
            }
            return ret;
        }


        ////////////////////////////////////////////////////////////////////////////////////////////////
        //CREATE   

        //Called from route and also seeder
        internal async Task<Widget> CreateAsync(Widget inObj)
        {
            await ValidateAsync(inObj, null);
            if (HasErrors)
                return null;
            else
            {
                //do stuff with widget
                Widget outObj = inObj;

                //Test get serial id visible id number from generator
                outObj.Serial = ServerBootConfig.WIDGET_SERIAL.GetNext();

                outObj.Tags = TagUtil.NormalizeTags(outObj.Tags);
                outObj.CustomFields = JsonUtil.CompactJson(outObj.CustomFields);
                //Save to db                              
                await ct.Widget.AddAsync(outObj);
                await ct.SaveChangesAsync();
                //Handle child and associated items:                
                await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, outObj.Id, BizType, AyaEvent.Created), ct);
                await SearchIndexAsync(outObj, true);
                await TagUtil.ProcessUpdateTagsInRepositoryAsync(ct, outObj.Tags, null);

                return outObj;
            }
        }




        ////////////////////////////////////////////////////////////////////////////////////////////////
        //DUPLICATE
        //

        internal async Task<Widget> DuplicateAsync(Widget dbObj)
        {

            Widget outObj = new Widget();
            CopyObject.Copy(dbObj, outObj);
            outObj.Name = Util.StringUtil.NameUniquify(outObj.Name, 255);
            outObj.Id = 0;
            outObj.ConcurrencyToken = 0;

            //Test get serial id visible id number from generator
            outObj.Serial = ServerBootConfig.WIDGET_SERIAL.GetNext();

            await ct.Widget.AddAsync(outObj);
            await ct.SaveChangesAsync();

            //Handle child and associated items:                
            await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, outObj.Id, BizType, AyaEvent.Created), ct);
            await SearchIndexAsync(outObj, true);
            await TagUtil.ProcessUpdateTagsInRepositoryAsync(ct, outObj.Tags, null);
            return outObj;

        }

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

        //put
        internal async Task<bool> PutAsync(Widget dbObj, Widget inObj)
        {

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

            //Replace the db object with the PUT object
            CopyObject.Copy(inObj, dbObj, "Id,Serial");

            dbObj.Tags = TagUtil.NormalizeTags(dbObj.Tags);
            dbObj.CustomFields = JsonUtil.CompactJson(dbObj.CustomFields);

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


            await ValidateAsync(dbObj, SnapshotOfOriginalDBObj);
            if (HasErrors)
                return false;

            //Log event and save context          
            await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObj.Id, BizType, AyaEvent.Modified), ct);
            await SearchIndexAsync(dbObj, false);
            await TagUtil.ProcessUpdateTagsInRepositoryAsync(ct, dbObj.Tags, SnapshotOfOriginalDBObj.Tags);

            return true;
        }

        //patch
        internal async Task<bool> PatchAsync(Widget dbObj, JsonPatchDocument<Widget> objectPatch, uint concurrencyToken)
        {
            //Validate Patch is allowed
            //Note: Id and Serial are all checked for and disallowed in the validate code by default
            if (!ValidateJsonPatch<Widget>.Validate(this, objectPatch)) return false;

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

            //Do the patching
            objectPatch.ApplyTo(dbObj);

            dbObj.Tags = TagUtil.NormalizeTags(dbObj.Tags);
            dbObj.CustomFields = JsonUtil.CompactJson(dbObj.CustomFields);

            ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = concurrencyToken;
            await ValidateAsync(dbObj, SnapshotOfOriginalDBObj);
            if (HasErrors)
                return false;

            //Log event and save context
            await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObj.Id, BizType, AyaEvent.Modified), ct);
            await SearchIndexAsync(dbObj, false);

            await TagUtil.ProcessUpdateTagsInRepositoryAsync(ct, dbObj.Tags, SnapshotOfOriginalDBObj.Tags);

            return true;
        }


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


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


        ////////////////////////////////////////////////////////////////////////////////////////////////
        //DELETE
        //
        internal async Task<bool> DeleteAsync(Widget dbObj)
        {
            //Determine if the object can be deleted, do the deletion tentatively
            //Probably also in here deal with tags and associated search text etc

            //NOT REQUIRED NOW BUT IF IN FUTURE ValidateCanDelete(dbObj);
            if (HasErrors)
                return false;
            ct.Widget.Remove(dbObj);
            await ct.SaveChangesAsync();

            //Log event 
            await EventLogProcessor.DeleteObjectLogAsync(UserId, BizType, dbObj.Id, dbObj.Name, ct);
            await Search.ProcessDeletedObjectKeywordsAsync(dbObj.Id, BizType);
            await TagUtil.ProcessDeleteTagsInRepositoryAsync(ct, dbObj.Tags);
            return true;
        }



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

        //Can save or update?
        private async Task ValidateAsync(Widget proposedObj, Widget currentObj)
        {

            //NOTE: In DB schema only name and serial are not nullable

            //run validation and biz rules
            bool isNew = currentObj == null;

            //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.Widget.AnyAsync(m => m.Name == proposedObj.Name && m.Id != proposedObj.Id))
                {
                    AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "Name");
                }
            }

            //Start date AND end date must both be null or both contain values
            if (proposedObj.StartDate == null && proposedObj.EndDate != null)
                AddError(ApiErrorCode.VALIDATION_REQUIRED, "StartDate");

            if (proposedObj.StartDate != null && proposedObj.EndDate == null)
                AddError(ApiErrorCode.VALIDATION_REQUIRED, "EndDate");

            //Start date before end date
            if (proposedObj.StartDate != null && proposedObj.EndDate != null)
                if (proposedObj.StartDate > proposedObj.EndDate)
                    AddError(ApiErrorCode.VALIDATION_STARTDATE_AFTER_ENDDATE, "StartDate");

            //Enum is valid value
            //Note: because of the way the flags work as powers of 2 and with the 1 being the first flag value, basically any value up to the "All" value will be valid
            //because you can make any number from 0 to all using any combination of the flags so the only thing that will fail is less than zero or greater than All
            if (!proposedObj.Roles.IsValid())
            {
                AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Roles");
            }

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

        }


        //Can delete?
        // private void ValidateCanDelete(Widget inObj)
        // {
        //     //whatever needs to be check to delete this object
        // }



        ////////////////////////////////////////////////////////////////////////////////////////////////
        //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.TestWidgetJob:
                    await ProcessTestJobAsync(job);
                    break;
                default:
                    throw new System.ArgumentOutOfRangeException($"WidgetBiz.HandleJob-> Invalid job type{job.JobType.ToString()}");
            }
        }


        /// <summary>
        /// /// Handle the test job
        /// </summary>
        /// <param name="job"></param>
        private async Task ProcessTestJobAsync(OpsJob job)
        {
            var sleepTime = 30 * 1000;
            //Simulate a long running job here            
            await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Running, ct);
            await JobsBiz.LogJobAsync(job.GId, $"WidgetBiz::ProcessTestJob started, sleeping for {sleepTime} seconds...", ct);
            //Uncomment this to test if the job prevents other routes from running
            //result is NO it doesn't prevent other requests, so we are a-ok for now
            await Task.Delay(sleepTime);
            await JobsBiz.LogJobAsync(job.GId, "WidgetBiz::ProcessTestJob done sleeping setting job to finished", ct);
            await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Completed, ct);

        }

        //Other job handlers here...


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

    }//eoc


}//eons

