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 System.Collections.Generic;

using Microsoft.Extensions.Logging;

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)
        {
            return new WidgetBiz(ct, UserIdFromContext.Id(httpContext.Items), UserLocaleIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
        }

        //Version for internal use
        internal static WidgetBiz GetBizInternal(AyContext ct)
        {
            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> GetNoLogAsync(long fetchId)
        {
            //This is simple so nothing more here, but often will be copying to a different output object or some other ops
            return await ct.Widget.SingleOrDefaultAsync(m => m.Id == fetchId);
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////
        //CREATE        
        internal async Task<Widget> CreateAsync(Widget inObj)
        {
            Validate(inObj, true);
            if (HasErrors)
                return null;
            else
            {
                //do stuff with widget
                Widget outObj = inObj;
                outObj.OwnerId = UserId;

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

                outObj.Tags = TagUtil.NormalizeTags(outObj.Tags);

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

                //Handle child and associated items:

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

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

                return outObj;

            }
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////
        //CREATE        
        internal Widget Create(AyContext TempContext, Widget inObj)
        {
            Validate(inObj, true);
            if (HasErrors)
                return null;
            else
            {
                //do stuff with widget
                Widget outObj = inObj;
                outObj.OwnerId = UserId;
                //Test get serial id visible id number from generator
                outObj.Serial = ServerBootConfig.WIDGET_SERIAL.GetNext();
                outObj.Tags = TagUtil.NormalizeTags(outObj.Tags);

                TempContext.Widget.Add(outObj);
                TempContext.SaveChanges();

                //Handle child and associated items:

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

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

                return outObj;

            }
        }

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

        //Get one
        internal async Task<Widget> 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.Widget.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("widget");
            f.
            AddField("id", "ID", AyDataType.Integer).
            AddField("name", "WidgetName", AyDataType.Text).
            AddField("serial", "WidgetSerial", AyDataType.Integer).
            AddField("notes", "WidgetNotes", AyDataType.Text).
            AddField("dollaramount", "WidgetDollarAmount", AyDataType.Decimal).
            AddField("active", "CommonActive", AyDataType.Bool).
            AddField("startdate", "WidgetStartDate", AyDataType.Date).
            AddField("count", "WidgetCount", AyDataType.Integer).
            AddField("tags", "Tags", AyDataType.Tags).
            AddField("enddate", "WidgetEndDate", AyDataType.Date);

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

        //get many (paged)
        internal async Task<ApiPagedResponse<Widget>> GetManyAsync(IUrlHelper Url, string routeName, PagingOptions pagingOptions)
        {
            pagingOptions.Offset = pagingOptions.Offset ?? PagingOptions.DefaultOffset;
            pagingOptions.Limit = pagingOptions.Limit ?? PagingOptions.DefaultLimit;

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

            //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, WidgetBiz.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.Widget
                                        .AsNoTracking()
                                        .FromSql(q)
                                        .Skip(pagingOptions.Offset.Value)
                                        .Take(pagingOptions.Limit.Value)
                                        .ToArrayAsync();

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

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

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


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

            // NameIdItem[] items;
            // int totalRecordCount = 0;

            //              //BUILD THE QUERY
            //             //base query
            //             var q = "SELECT id, name FROM AWIDGET ";

            //             //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, WidgetBiz.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.Widget
            //                                         .AsNoTracking()
            //                                         .FromSql(q)
            //                                         .Skip(pagingOptions.Offset.Value)
            //                                         .Take(pagingOptions.Limit.Value)
            //                                         .ToArrayAsync();

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

            // if (!string.IsNullOrWhiteSpace(q))
            // {
            //     items = await ct.Widget
            //                     .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.Widget.Where(m => EF.Functions.ILike(m.Name, q)).CountAsync();
            // }
            // else
            // {
            //     items = await ct.Widget
            //                 .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.Widget.CountAsync();
            // }

            var ret = PickListFetcher.GetPickList(ct, UserId, pagingOptions, "awidget");

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

            ApiPagedResponse<NameIdItem> pr = new ApiPagedResponse<NameIdItem>(ret.Items, pageLinks);
            return pr;
        }




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

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

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

            dbObj.Tags = TagUtil.NormalizeTags(dbObj.Tags);

            //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, false);
            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.Notes, dbObj.Name, dbObj.Serial.ToString());
            var SearchParams = new Search.SearchIndexProcessObjectParameters(UserLocaleId, dbObj.Id, BizType, dbObj.Name);
            SearchParams.AddWord(dbObj.Notes).AddWord(dbObj.Name).AddWord(dbObj.Serial).AddWord(dbObj.Tags);
            Search.ProcessNewObjectKeywords(SearchParams);

            return true;
        }

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

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

            dbObj.Tags = TagUtil.NormalizeTags(dbObj.Tags);

            ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = concurrencyToken;
            Validate(dbObj, false);
            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.Notes, dbObj.Name, dbObj.Serial.ToString());
            var SearchParams = new Search.SearchIndexProcessObjectParameters(UserLocaleId, dbObj.Id, BizType, dbObj.Name);
            SearchParams.AddWord(dbObj.Notes).AddWord(dbObj.Name).AddWord(dbObj.Serial).AddWord(dbObj.Tags);
            Search.ProcessNewObjectKeywords(SearchParams);

            return true;
        }


        ////////////////////////////////////////////////////////////////////////////////////////////////
        //DELETE
        //
        internal bool Delete(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

            ValidateCanDelete(dbObj);
            if (HasErrors)
                return false;
            ct.Widget.Remove(dbObj);
            ct.SaveChanges();

            //Delete sibling objects

            //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(Widget inObj, bool isNew)
        {
            //run validation and biz rules
            if (isNew)
            {
                // //NEW widgets must be active
                // if (inObj.Active == null || ((bool)inObj.Active) == false)
                // {
                //     AddError(ValidationErrorType.InvalidValue, "Active", "New widget must be active");
                // }
            }

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

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

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

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

            if (inObj.StartDate != null && inObj.EndDate == null)
                AddError(ValidationErrorType.RequiredPropertyEmpty, "EndDate");

            //Start date before end date
            if (inObj.StartDate != null && inObj.EndDate != null)
                if (inObj.StartDate > inObj.EndDate)
                    AddError(ValidationErrorType.StartDateMustComeBeforeEndDate, "StartDate");

            //Enum is valid value

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


            return;
        }


        //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            
            JobsBiz.UpdateJobStatus(job.GId, JobStatus.Running, ct);
            JobsBiz.LogJob(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);
            JobsBiz.LogJob(job.GId, "WidgetBiz::ProcessTestJob done sleeping setting job to finished", ct);
            JobsBiz.UpdateJobStatus(job.GId, JobStatus.Completed, ct);

        }

        //Other job handlers here...


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

    }//eoc


}//eons

