using AsbCloudApp.Data;
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.Tasks;
using System.Threading;
using AsbCloudApp.Repositories;

namespace AsbCloudInfrastructure.Repository
{
#nullable enable
    /// <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()
        {
            var allCategories = memoryCache
                .GetOrCreateBasic<WellOperationCategory>(db);

            var parentIds = allCategories
                .Select(o => o.IdParent)
                .Distinct();

            var operationCategories = allCategories
                .Where(o => !parentIds.Contains(o.Id))
                .OrderBy(o => o.IdParent)
                .ThenBy(o => o.Name);

            var result = operationCategories.Adapt<IEnumerable<WellOperationCategoryDto>>();
            return result;
        }

        /// <inheritdoc/>
        public IDictionary<int, string> GetSectionTypes()
            => memoryCache
            .GetOrCreateBasic<WellSectionType>(db)
            .ToDictionary(s => s.Id, s => s.Caption);

        /// <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.ToListAsync(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)
        {
            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()
                .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
                });
            var defaultId = 0;
            while (dtos.Any(x => x.IdParent != null))
            {
                defaultId--;
                dtos = dtos
                .GroupBy(o => o.IdParent)
                .Select(g => new WellGroupOpertionDto
                {
                    IdCategory = g.Key ?? defaultId,
                    Category = g.Key.HasValue ? parentRelationDictionary[g.Key.Value].Name : "unknown",
                    Count = g.Sum(o => o.Count),
                    DeltaDepth = g.Sum(o => o.DeltaDepth),
                    TotalMinutes = g.Sum(o => o.TotalMinutes),
                    Items = g.ToList(),
                    IdParent = g.Key.HasValue ? parentRelationDictionary[g.Key.Value].IdParent : defaultId,
                });
            }
            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)
                .Select(o => new WellOperationDto
                {
                    Id = o.Id,
                    IdType = o.IdType,
                    IdWell = o.IdWell,
                    IdWellSectionType = o.IdWellSectionType,
                    IdCategory = o.IdCategory,

                    //o.Well,
                    //o.OperationCategory,
                    CategoryName = o.WellSectionType.Caption,
                    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 = db.WellOperations
                        .Where(subOp => subOp.IdWell == request.IdWell)
                        .Where(subOp => subOp.IdType == 1)
                        .Where(subOp => WellOperationCategory.NonProductiveTimeSubIds.Contains(subOp.IdCategory))
                        .Where(subOp => subOp.DateStart <= o.DateStart)
                        .Select(subOp => subOp.DurationHours)
                        .Sum(),

                    Day = (o.DateStart - db.WellOperations
                        .Where(subOp => subOp.IdWell == request.IdWell)
                        .Where(subOp => subOp.IdType == o.IdType)
                        .Where(subOp => subOp.DateStart <= o.DateStart)
                        .Min(subOp => subOp.DateStart))
                        .TotalHours,
                });

            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.LeDate.HasValue)
            {
                var leDateOffset = request.LeDate.Value.ToUtcDateTimeOffset(timezone.Hours);
                query = query.Where(e => e.DateStart <= leDateOffset);
            }

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

            return query;
        }
    }
#nullable disable
}