using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using PoweredSoft.DynamicLinq;
using PoweredSoft.DynamicLinq.Fluent;
using PoweredSoft.DynamicQuery.Core;

namespace PoweredSoft.DynamicQuery
{
    public class QueryHandler : QueryHandlerBase, IQueryHandler
    {
        protected virtual IQueryExecutionResult<TRecord> FinalExecute<TSource, TRecord>()
        {
            CommonBeforeExecute<TSource>();
            return HasGrouping ? ExecuteGrouping<TSource, TRecord>() : ExecuteNoGrouping<TSource, TRecord>();
        }

        protected virtual IQueryExecutionResult<TRecord> ExecuteGrouping<TSource, TRecord>()
        {
            var result = new QueryExecutionGroupResult<TRecord>();

            // preserve queryable.
            var queryableAfterFilters = CurrentQueryable;

            result.TotalRecords = queryableAfterFilters.LongCount();
            CalculatePageCount(result);

            // intercept groups in advance to avoid doing it more than once :)
            var finalGroups = Criteria.Groups.Select(g => InterceptGroup<TSource>(g)).ToList();

            // get the aggregates.
            var aggregateResults = FetchAggregates<TSource>(finalGroups);

            // sorting.
            finalGroups.ForEach(fg => Criteria.Sorts.Insert(0, new Sort(fg.Path, fg.Ascending)));

            // apply sorting and paging.
            ApplySorting<TSource>();
            ApplyPaging<TSource>();

            // create group & select expression.
            CurrentQueryable = CurrentQueryable.GroupBy(QueryableUnderlyingType, gb => finalGroups.ForEach((fg, index) => gb.Path(fg.Path, $"Key_{index}")));
            CurrentQueryable = CurrentQueryable.Select(sb =>
            {
                finalGroups.ForEach((fg, index) => sb.Key($"Key_{index}", $"Key_{index}"));
                sb.ToList("Records");
            });

            // loop through the grouped records.
            var groupRecords = CurrentQueryable.ToDynamicClassList();

            // now join them into logical collections
            var lastLists = new List<(List<TSource> source, IGroupQueryResult<TRecord> group)>();
            result.Groups = RecursiveRegroup<TSource, TRecord>(groupRecords, aggregateResults, Criteria.Groups.First(), lastLists);

            // intercept grouped by.
            QueryInterceptToGrouped<TSource, TRecord>(lastLists).Wait();

            result.Aggregates = CalculateTotalAggregate<TSource>(queryableAfterFilters);
            return result;
        }
        protected virtual List<IAggregateResult> CalculateTotalAggregate<TSource>(IQueryable queryableAfterFilters)
        {
            if (!Criteria.Aggregates.Any())
                return null;

            IQueryable selectExpression = CreateTotalAggregateSelectExpression<TSource>(queryableAfterFilters);
            var aggregateResult = selectExpression.ToDynamicClassList().FirstOrDefault();
            return MaterializeCalculateTotalAggregateResult(aggregateResult);
        }
        
        protected virtual List<List<DynamicClass>> FetchAggregates<TSource>(List<IGroup> finalGroups)
        {
            if (!Criteria.Aggregates.Any())
                return null;
            
            var previousGroups = new List<IGroup>();
            var ret = finalGroups.Select(fg =>
            {
                IQueryable selectExpression = CreateFetchAggregateSelectExpression<TSource>(fg, previousGroups);
                var aggregateResult = selectExpression.ToDynamicClassList();
                previousGroups.Add(fg);
                return aggregateResult;
            }).ToList();
            return ret;
        }

        protected virtual IQueryExecutionResult<TRecord> ExecuteNoGrouping<TSource, TRecord>()
        {
            var result = new QueryExecutionResult<TRecord>();

            // after filter queryable
            var afterFilterQueryable = CurrentQueryable;

            // total records.
            result.TotalRecords = afterFilterQueryable.LongCount();
            CalculatePageCount(result);

            // sorts and paging.
            ApplySorting<TSource>();
            ApplyPaging<TSource>();

            // data.
            var entities = ((IQueryable<TSource>)CurrentQueryable).ToList();
            var records = InterceptConvertTo<TSource, TRecord>(entities).Result;
            result.Data = records;

            // aggregates.
            result.Aggregates = CalculateTotalAggregate<TSource>(afterFilterQueryable);

            return result;
        }

        public IQueryExecutionResult<TSource> Execute<TSource>(IQueryable<TSource> queryable, IQueryCriteria criteria)
        {
            Reset(queryable, criteria);
            return FinalExecute<TSource, TSource>();
        }

        public IQueryExecutionResult<TRecord> Execute<TSource, TRecord>(IQueryable<TSource> queryable, IQueryCriteria criteria)
        {
            Reset(queryable, criteria);
            return FinalExecute<TSource, TRecord>();
        }
    }
}