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

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 
        /// 
        ///

        //Get without logging
        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);
        }

        //Get one with logging
        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;
        }


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


        //route linked version for external api access
        internal async Task<Widget> CreateAsync(Widget inObj)
        {
            Validate(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);

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

                //Handle child and associated items:                
                EventLogProcessor.LogEventToDatabase(new Event(UserId, outObj.Id, BizType, AyaEvent.Created), ct);
                SearchIndex(outObj, true);
                TagUtil.ProcessUpdateTagsInRepository(ct, outObj.Tags, null);
                return outObj;
            }
        }


        //Internal version for seeding
        internal Widget Create(AyContext TempContext, Widget inObj)
        {
            Validate(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);

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

                //Handle child and associated items:
                EventLogProcessor.LogEventToDatabase(new Event(UserId, outObj.Id, BizType, AyaEvent.Created), TempContext);
                SearchIndex(outObj, true);
                TagUtil.ProcessUpdateTagsInRepository(TempContext, 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:                
            EventLogProcessor.LogEventToDatabase(new Event(UserId, outObj.Id, BizType, AyaEvent.Created), ct);
            SearchIndex(outObj, true);
            TagUtil.ProcessUpdateTagsInRepository(ct, outObj.Tags, null);
            return outObj;

        }

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

        //put
        internal bool Put(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;


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

            //Associated items          
            EventLogProcessor.LogEventToDatabase(new Event(UserId, dbObj.Id, BizType, AyaEvent.Modified), ct);
            SearchIndex(dbObj, false);
            TagUtil.ProcessUpdateTagsInRepository(ct, dbObj.Tags, SnapshotOfOriginalDBObj.Tags);

            return true;
        }

        //patch
        internal bool Patch(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;
            Validate(dbObj, SnapshotOfOriginalDBObj);
            if (HasErrors)
                return false;

            //Associated items
            EventLogProcessor.LogEventToDatabase(new Event(UserId, dbObj.Id, BizType, AyaEvent.Modified), ct);
            SearchIndex(dbObj, false);

            TagUtil.ProcessUpdateTagsInRepository(ct, dbObj.Tags, SnapshotOfOriginalDBObj.Tags);

            return true;
        }


        private void SearchIndex(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)
                Search.ProcessNewObjectKeywords(SearchParams);
            else
                Search.ProcessUpdatedObjectKeywords(SearchParams);
        }


        ////////////////////////////////////////////////////////////////////////////////////////////////
        //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();

            //Associated items
            EventLogProcessor.DeleteObject(UserId, BizType, dbObj.Id, dbObj.Name, ct);
            ct.SaveChanges();
            Search.ProcessDeletedObjectKeywords(dbObj.Id, BizType);
            TagUtil.ProcessDeleteTagsInRepository(ct, dbObj.Tags);
            return true;
        }



        ////////////////////////////////////////////////////////////////////////////////////////////////
        // LISTS
        //




        //get many (paged)
        internal async Task<ApiPagedResponse> GetList(IUrlHelper Url, string routeName, ListOptions listOptions)
        {



            //TODO: Get template (MOCKED FOR NOW UNTIL PROOF OF CONCEPT)          
            var MOCK_WIDGET_DISPLAY_TEMPLATE_JSON = @"
            {
                ""full"":[""WidgetName"",""WidgetSerial"",""WidgetDollarAmount"",""WidgetRoles"",""WidgetStartDate"",""Active""],
                ""mini"":[""WidgetName"",""WidgetSerial""]
            }
            ";

            //BUILD THE QUERY           

            //SELECT FRAGMENT COLUMNS FROM TEMPLATE
            //"select clm,clm,clm"
            var qSelectColumns = SqlSelectBuilder.Build(ObjectFields.WIDGET_KEY, MOCK_WIDGET_DISPLAY_TEMPLATE_JSON, listOptions.Mini);

            var qFrom = "FROM AWIDGET";

            //FILTERED?
            DataFilter TheFilter = null;
            if (listOptions.DataFilterId > 0)
            {
                TheFilter = await ct.DataFilter.FirstOrDefaultAsync(x => x.Id == listOptions.DataFilterId);
            }

            //WHERE CLAUSE - FILTER 
            var qWhere = string.Empty;
            if (listOptions.DataFilterId > 0)
            {
                qWhere = SqlFilterCriteriaBuilder.DataFilterToSQLCriteria(TheFilter, ObjectFields.ObjectFieldsList(ObjectFields.WIDGET_KEY), UserId);
            }

            //ORDER BY CLAUSE - SORT 
            var qOrderBy = string.Empty;
            if (listOptions.DataFilterId > 0)
            {
                //BUILD ORDER BY AND APPEND IT
                qOrderBy = SqlFilterOrderByBuilder.DataFilterToSQLOrderBy(TheFilter);
            }
            else
            {
                //GET DEFAULT ORDER BY
                qOrderBy = SqlFilterOrderByBuilder.DefaultGetManyOrderBy();
            }

            //LIMIT AND OFFSET CLAUSE - PAGING
            listOptions.Offset = listOptions.Offset ?? ListOptions.DefaultOffset;
            listOptions.Limit = listOptions.Limit ?? ListOptions.DefaultLimit;
            var qLimitOffset = $"LIMIT {listOptions.Limit} OFFSET {listOptions.Offset}";

            //PUT IT ALL TOGETHER
            string qDataQuery = string.Empty;
            string qTotalRecordsQuery = string.Empty;
            if (TheFilter != null)
            {
                qDataQuery = $"{qSelectColumns} {qFrom} {qWhere} {qOrderBy} {qLimitOffset}";
                qTotalRecordsQuery = $"SELECT COUNT(*) {qFrom} {qWhere}";
            }
            else
            {
                qDataQuery = $"{qSelectColumns} {qFrom} {qLimitOffset}";
                qTotalRecordsQuery = $"SELECT COUNT(*) {qFrom}";
            }

         
            //RETURN OBJECTS
            List<object[]> items = new List<object[]>();
            long totalRecordCount = 0;

            //RUN THE QUERY
            using (var command = ct.Database.GetDbConnection().CreateCommand())
            {

                ct.Database.OpenConnection();
                command.CommandText = qDataQuery;
                using (var dr = command.ExecuteReader())
                {
                    while (dr.Read())
                    {
                        object[] row = new object[dr.FieldCount];
                        dr.GetValues(row);
                        items.Add(row);
                    }
                }

                command.CommandText = qTotalRecordsQuery;
                using (var dr = command.ExecuteReader())
                {
                    if (dr.Read())
                    {
                        totalRecordCount = dr.GetInt64(0);
                    }                 
                }
            }

//TODO: Build the return object in a clean format
//rows:{[ {},{v:"Green mechanics",id:32},{v:"...notes..."},{v:"42",id:42}, ...thousands more etc.... ]}

            
            

            //BUILD THE PAGING LINKS PORTION
            var pageLinks = new PaginationLinkBuilder(Url, routeName, null, listOptions, totalRecordCount).PagingLinksObject();

            //BUILD THE COLUMNS RETURN PROPERTY JSON FRAGMENT          
            string ColumnsJSON = string.Empty;
            if (listOptions.Mini)
            {
                ColumnsJSON = ObjectFields.GenerateMINIListColumnsJSON(AyaType.Widget);
            }
            else
            {
                ColumnsJSON = ObjectFields.GenerateListColumnsJSONFromTemplate(AyaType.Widget, ObjectFields.WIDGET_KEY, MOCK_WIDGET_DISPLAY_TEMPLATE_JSON);
            }

            //TODO: BUILD THE RETURN LIST OF DATA ITEMS
            //If mini format all desired columns in order into the single mini return display (and set the only other return field which is ID)

            //If wide then format the fields in oder chosen (grid sort and filter template has already done that part above)

            //TODO: Genericize the above block of building return when it's working as this code needs to be central and optimized as much as possible


            ApiPagedResponse pr = new ApiPagedResponse(items, pageLinks, ColumnsJSON);
            return pr;
        }


        //         //get many (paged)
        //         internal async Task<ApiPagedResponse<Widget>> GetManyAsync(IUrlHelper Url, string routeName, ListOptions pagingOptions)
        //         {
        //             pagingOptions.Offset = pagingOptions.Offset ?? ListOptions.DefaultOffset;
        //             pagingOptions.Limit = pagingOptions.Limit ?? ListOptions.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.DefaultGetManyOrderBy();
        //             }


        // #pragma warning disable EF1000

        //             var items = await ct.Widget
        //                                         .FromSqlRaw(q)
        //                                         .AsNoTracking()
        //                                         .Skip(pagingOptions.Offset.Value)
        //                                         .Take(pagingOptions.Limit.Value)
        //                                         .ToArrayAsync();

        //             var totalRecordCount = await ct.Widget
        //                                                     .FromSqlRaw(q)
        //                                                     .AsNoTracking()
        //                                                     .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;
        //         }


        // /// <summary>
        // /// Get PickList
        // /// </summary>
        // /// <param name="Url"></param>
        // /// <param name="routeName"></param>
        // /// <param name="pagingOptions"></param>
        // /// <returns></returns>
        // internal ApiPagedResponse<NameIdItem> GetPickList(IUrlHelper Url, string routeName, ListOptions pagingOptions)
        // {
        //     pagingOptions.Offset = pagingOptions.Offset ?? ListOptions.DefaultOffset;
        //     pagingOptions.Limit = pagingOptions.Limit ?? ListOptions.DefaultLimit;
        //     var ret = PickListFetcher.GetPickList(ct, UserId, pagingOptions, FilterOptions(), "awidget");
        //     var pageLinks = new PaginationLinkBuilder(Url, routeName, null, pagingOptions, ret.TotalRecordCount).PagingLinksObject();
        //     ApiPagedResponse<NameIdItem> pr = new ApiPagedResponse<NameIdItem>(ret.Items, pageLinks);
        //     return pr;
        // }





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

        //Can save or update?
        private void Validate(Widget proposedObj, Widget currentObj)
        {

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

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

            // if (isNew)
            // {
            //     //WARNING: this is not really the "current" object, it's been modified already by caller

            //     // //NEW widgets must be active
            //     // if (inObj.Active == null || ((bool)inObj.Active) == false)
            //     // {
            //     //     AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Active", "New widget must be active");
            //     // }
            // }


            //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 (ct.Widget.Any(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 = ct.FormCustom.SingleOrDefault(x => x.FormKey == ObjectFields.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);
            }

            //for debug purposes
#if (DEBUG)
            //TESTING
            //make a fake server error for ui testing purposes
            // if (proposedObj.Count == 666)
            // {
            //     AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Count", "Test field server error");
            // }

            //removed because seeding huge kept triggering this
            // if (proposedObj.DollarAmount == 666.66M)
            // {
            //     AddError(ApiErrorCode.INVALID_OPERATION, null, "This is a test of a general server error");
            // }

            //TESTING
#endif

            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

