using AsbCloudApp.Data;
using AsbCloudApp.Repositories;
using AsbCloudApp.Requests;
using AsbCloudApp.Services;
using AsbCloudDb;
using AsbCloudDb.Model;
using Mapster;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace AsbCloudInfrastructure.Repository
{

    /// <summary>
    /// репозиторий операций по скважине
    /// </summary>
    public class WellOperationRepository : IWellOperationRepository
    {
        private readonly IAsbCloudDbContext db;
        private readonly IMemoryCache memoryCache;
        private readonly IWellService wellService;
        private static Dictionary<int, DateTimeOffset?>? firstOperationsCache = null;

        public WellOperationRepository(IAsbCloudDbContext db, IMemoryCache memoryCache, IWellService wellService)
        {
            this.db = db;
            this.memoryCache = memoryCache;
            this.wellService = wellService;
        }

        /// <inheritdoc/>
        public IEnumerable<WellOperationCategoryDto> GetCategories(bool includeParents)
        {
            var categories = memoryCache
                .GetOrCreateBasic(db.Set<WellOperationCategory>());

            if (!includeParents)
            {
                var parentIds = categories
                    .Select(o => o.IdParent)
                    .Distinct();

                categories = categories
                    .Where(o => !parentIds.Contains(o.Id));
            }

            var result = categories
                .OrderBy(o => o.Name)                
                .Adapt<IEnumerable<WellOperationCategoryDto>>();

            return result;
        }

        /// <inheritdoc/>
        public IEnumerable<WellSectionTypeDto> GetSectionTypes() =>
            memoryCache
               .GetOrCreateBasic(db.Set<WellSectionType>())
               .OrderBy(s => s.Order)
               .Select(s => s.Adapt<WellSectionTypeDto>());



        public async Task<WellOperationPlanDto> GetOperationsPlanAsync(int idWell, DateTime? currentDate, CancellationToken token)
        {
            var timezone = wellService.GetTimezone(idWell);
            var request = new WellOperationRequest()
            {
                IdWell = idWell,
                OperationType = WellOperation.IdOperationTypePlan,
            };

            var entities = await BuildQuery(request)
                .AsNoTracking()
                .ToArrayAsync(token)
                .ConfigureAwait(false);

            var dateLastAssosiatedPlanOperation = await GetDateLastAssosiatedPlanOperationAsync(idWell, currentDate, timezone.Hours, token);

            var result = new WellOperationPlanDto()
            {
                WellOperationsPlan = entities,
                DateLastAssosiatedPlanOperation = dateLastAssosiatedPlanOperation
            };

            return result;
        }

        private async Task<DateTime?> GetDateLastAssosiatedPlanOperationAsync(
            int idWell, 
            DateTime? lessThenDate, 
            double timeZoneHours, 
            CancellationToken token)
        {
            if (lessThenDate is null)            
                return null;
            
            var currentDateOffset = lessThenDate.Value.ToUtcDateTimeOffset(timeZoneHours);
            var timeZoneOffset = TimeSpan.FromHours(timeZoneHours);

            var lastFactOperation = await db.WellOperations
                .Where(o => o.IdWell == idWell)
                .Where(o => o.IdType == WellOperation.IdOperationTypeFact)
                .Where(o => o.IdPlan != null)
                .Where(o => o.DateStart < currentDateOffset)
                .Include(x => x.OperationPlan)
                .OrderByDescending(x => x.DateStart)
                .FirstOrDefaultAsync(token)
                .ConfigureAwait(false);

            if (lastFactOperation is not null)
                return DateTime.SpecifyKind(lastFactOperation.OperationPlan.DateStart.UtcDateTime + timeZoneOffset, DateTimeKind.Unspecified);
            return null;
        }

        /// <inheritdoc/>
        public DateTimeOffset? FirstOperationDate(int idWell)
        {
            if (firstOperationsCache is null)
            {
                var query = db.WellOperations
                .GroupBy(o => o.IdWell)
                .Select(g => new Tuple<int, DateTimeOffset?, DateTimeOffset?>
                (
                    g.Key,
                    g.Where(o => o.IdType == WellOperation.IdOperationTypePlan).Min(o => o.DateStart),
                    g.Where(o => o.IdType == WellOperation.IdOperationTypeFact).Min(o => o.DateStart)
                ));

                firstOperationsCache = query
                    .ToDictionary(f => f.Item1, f => f.Item3 ?? f.Item2);
            }

            return firstOperationsCache?.GetValueOrDefault(idWell);
        }

        /// <inheritdoc/>
        public async Task<IEnumerable<WellOperationDto>> GetAsync(
            WellOperationRequest request,
            CancellationToken token)
        {
            var query = BuildQuery(request)
                .AsNoTracking();
            var result = await query.ToArrayAsync(token);
            return result;
        }

        /// <inheritdoc/>
        public async Task<PaginationContainer<WellOperationDto>> GetPageAsync(
            WellOperationRequest request,
            CancellationToken token)
        {
            var query = BuildQuery(request)
                .AsNoTracking();

            var result = new PaginationContainer<WellOperationDto>
            {
                Skip = request.Skip ?? 0,
                Take = request.Take ?? 32,
                Count = await query.CountAsync(token).ConfigureAwait(false),
            };

            query = query
                .Skip(result.Skip)
                .Take(result.Take);

            result.Items = await query.ToArrayAsync(token);
            return result;
        }

        /// <inheritdoc/>
        public async Task<WellOperationDto?> GetOrDefaultAsync(int id,
            CancellationToken token)
        {
            var entity = await db.WellOperations
                .Include(s => s.WellSectionType)
                .Include(s => s.OperationCategory)
                .FirstOrDefaultAsync(e => e.Id == id, token)
                .ConfigureAwait(false);

            if (entity is null)
                return null;

            var timezone = wellService.GetTimezone(entity.IdWell);

            var dto = entity.Adapt<WellOperationDto>();
            dto.WellSectionTypeName = entity.WellSectionType.Caption;
            dto.DateStart = entity.DateStart.ToRemoteDateTime(timezone.Hours);
            dto.CategoryName = entity.OperationCategory.Name;
            return dto;
        }

        /// <inheritdoc/>
        public async Task<IEnumerable<WellGroupOpertionDto>> GetGroupOperationsStatAsync(
            WellOperationRequest request,
            CancellationToken token)
        {
            // TODO: Rename controller method
            request.OperationType = WellOperation.IdOperationTypeFact;
            var query = BuildQuery(request);
            var entities = await query
                .Select(o => new
                {
                    o.IdCategory,
                    DurationMinutes = o.DurationHours * 60,
                    DurationDepth = o.DepthEnd - o.DepthStart
                })
                .ToListAsync(token);
            var parentRelationDictionary = GetCategories(true)
                .ToDictionary(c => c.Id, c => new
                {
                    c.Name,
                    c.IdParent
                });

            var dtos = entities
                .GroupBy(o => o.IdCategory)
                .Select(g => new WellGroupOpertionDto
                {
                    IdCategory = g.Key,
                    Category = parentRelationDictionary[g.Key].Name,
                    Count = g.Count(),
                    MinutesAverage = g.Average(o => o.DurationMinutes),
                    MinutesMin = g.Min(o => o.DurationMinutes),
                    MinutesMax = g.Max(o => o.DurationMinutes),
                    TotalMinutes = g.Sum(o => o.DurationMinutes),
                    DeltaDepth = g.Sum(o => o.DurationDepth),
                    IdParent = parentRelationDictionary[g.Key].IdParent
                });
            
            while (dtos.All(x => x.IdParent != null))
            {
                dtos = dtos
                .GroupBy(o => o.IdParent!)
                .Select(g => {
                    var idCategory = g.Key ?? int.MinValue;
                    var category = parentRelationDictionary.GetValueOrDefault(idCategory);
                    var newDto = new WellGroupOpertionDto
                    {
                        IdCategory = idCategory,
                        Category = category?.Name ?? "unknown",
                        Count = g.Sum(o => o.Count),
                        DeltaDepth = g.Sum(o => o.DeltaDepth),
                        TotalMinutes = g.Sum(o => o.TotalMinutes),
                        Items = g.ToList(),
                        IdParent = category?.IdParent,
                    };
                    return newDto;
                });
            }
            return dtos;
        }

        /// <inheritdoc/>
        public async Task<int> InsertRangeAsync(
            IEnumerable<WellOperationDto> wellOperationDtos,
            CancellationToken token)
        {
            var firstOperation = wellOperationDtos
                .FirstOrDefault();
            if (firstOperation is null)
                return 0;

            var idWell = firstOperation.IdWell;

            var timezone = wellService.GetTimezone(idWell);
            foreach (var dto in wellOperationDtos)
            {
                var entity = dto.Adapt<WellOperation>();
                entity.Id = default;
                entity.DateStart = dto.DateStart.ToUtcDateTimeOffset(timezone.Hours);
                entity.IdWell = idWell;
                db.WellOperations.Add(entity);
            }

            return await db.SaveChangesAsync(token)
                .ConfigureAwait(false);
        }

        /// <inheritdoc/>
        public async Task<int> UpdateAsync(
            WellOperationDto dto, CancellationToken token)
        {
            var timezone = wellService.GetTimezone(dto.IdWell);
            var entity = dto.Adapt<WellOperation>();
            entity.DateStart = dto.DateStart.ToUtcDateTimeOffset(timezone.Hours);
            db.WellOperations.Update(entity);
            return await db.SaveChangesAsync(token)
                .ConfigureAwait(false);
        }

        /// <inheritdoc/>
        public async Task<int> DeleteAsync(IEnumerable<int> ids,
            CancellationToken token)
        {
            var query = db.WellOperations.Where(e => ids.Contains(e.Id));
            db.WellOperations.RemoveRange(query);
            return await db.SaveChangesAsync(token)
                .ConfigureAwait(false);
        }

        /// <summary>
        /// В результате попрежнему требуется конвертировать дату
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        private IQueryable<WellOperationDto> BuildQuery(WellOperationRequest request)
        {
            var timezone = wellService.GetTimezone(request.IdWell);
            var timeZoneOffset = TimeSpan.FromHours(timezone.Hours);

            var query = db.WellOperations
                .Include(s => s.WellSectionType)
                .Include(s => s.OperationCategory)
                .Where(o => o.IdWell == request.IdWell);


            if (request.OperationType.HasValue)
                query = query.Where(e => e.IdType == request.OperationType.Value);

            if (request.SectionTypeIds?.Any() == true)
                query = query.Where(e => request.SectionTypeIds.Contains(e.IdWellSectionType));

            if (request.OperationCategoryIds?.Any() == true)
                query = query.Where(e => request.OperationCategoryIds.Contains(e.IdCategory));

            if (request.GeDepth.HasValue)
                query = query.Where(e => e.DepthEnd >= request.GeDepth.Value);

            if (request.LeDepth.HasValue)
                query = query.Where(e => e.DepthEnd <= request.LeDepth.Value);

            if (request.GeDate.HasValue)
            {
                var geDateOffset = request.GeDate.Value.ToUtcDateTimeOffset(timezone.Hours);
                query = query.Where(e => e.DateStart >= geDateOffset);
            }

            if (request.LtDate.HasValue)
            {
                var ltDateOffset = request.LtDate.Value.ToUtcDateTimeOffset(timezone.Hours);
                query = query.Where(e => e.DateStart < ltDateOffset);
            }

            var currentWellOperations = db.WellOperations
                .Where(subOp => subOp.IdWell == request.IdWell);

            var wellOperationsWithCategoryNPT = currentWellOperations
                .Where(subOp => subOp.IdType == 1)
                .Where(subOp => WellOperationCategory.NonProductiveTimeSubIds.Contains(subOp.IdCategory));

            var result = query.Select(o => new WellOperationDto
            {
                Id = o.Id,
                IdPlan = o.IdPlan,
                IdType = o.IdType,
                IdWell = o.IdWell,
                IdWellSectionType = o.IdWellSectionType,
                IdCategory = o.IdCategory,
                IdParentCategory = o.OperationCategory.IdParent,

                CategoryName = o.OperationCategory.Name,
                WellSectionTypeName = o.WellSectionType.Caption,

                DateStart = DateTime.SpecifyKind(o.DateStart.UtcDateTime + timeZoneOffset, DateTimeKind.Unspecified),
                DepthStart = o.DepthStart,
                DepthEnd = o.DepthEnd,
                DurationHours = o.DurationHours,
                CategoryInfo = o.CategoryInfo,
                Comment = o.Comment,

                NptHours = wellOperationsWithCategoryNPT
                         .Where(subOp => subOp.DateStart <= o.DateStart)
                         .Select(subOp => subOp.DurationHours)
                         .Sum(),

                Day = (o.DateStart - currentWellOperations
                         .Where(subOp => subOp.IdType == o.IdType)
                         .Where(subOp => subOp.DateStart <= o.DateStart)
                         .Min(subOp => subOp.DateStart))
                         .TotalDays,
                IdUser = o.IdUser,
                LastUpdateDate = o.LastUpdateDate.ToOffset(TimeSpan.FromHours(timezone.Hours))
            });

            if (request.SortFields?.Any() == true)
            {
                result = result.SortBy(request.SortFields);
            }
            else
            {
                result = result
                    .OrderBy(e => e.DateStart)
                    .ThenBy(e => e.DepthEnd)
                    .ThenBy(e => e.Id);
            };

            return result;
        }
    }

}