using AsbCloudApp.Data;
using AsbCloudApp.Data.WellOperation;
using AsbCloudApp.Exceptions;
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;

public class WellOperationRepository : CrudRepositoryBase<WellOperationDto, WellOperation>,
    IWellOperationRepository
{
    private readonly IMemoryCache memoryCache;
    private readonly IWellOperationCategoryRepository wellOperationCategoryRepository;
    private readonly IWellService wellService;

    public WellOperationRepository(IAsbCloudDbContext context,
        IMemoryCache memoryCache,
        IWellOperationCategoryRepository wellOperationCategoryRepository,
        IWellService wellService)
        : base(context, dbSet => dbSet.Include(e => e.WellSectionType)
            .Include(e => e.OperationCategory))
    {
        this.memoryCache = memoryCache;
        this.wellOperationCategoryRepository = wellOperationCategoryRepository;
        this.wellService = wellService;
    }

    public IEnumerable<WellSectionTypeDto> GetSectionTypes() =>
        memoryCache
            .GetOrCreateBasic(dbContext.WellSectionTypes)
            .OrderBy(s => s.Order)
            .Select(s => s.Adapt<WellSectionTypeDto>());

    public async Task<IEnumerable<WellOperationDto>> GetAsync(WellOperationRequest request, CancellationToken token)
    {
        var (items, _) = await GetPrivateAsync(request, token);
        return items;
    }

    public async Task<PaginationContainer<WellOperationDto>> GetPageAsync(WellOperationRequest request, CancellationToken token)
    {
        var skip = request.Skip ?? 0;
        var take = request.Take ?? 32;

        var (items, count) = await GetPrivateAsync(request, token);

        var paginationContainer = new PaginationContainer<WellOperationDto>
        {
            Skip = skip,
            Take = take,
            Count = count,
            Items = items
        };

        return paginationContainer;
    }

    public async Task<IEnumerable<WellGroupOpertionDto>> GetGroupOperationsStatAsync(WellOperationRequest request, CancellationToken token)
    {
        var query = BuildQuery(request, token);
        var entities = (await query)
            .Select(o => new
            {
                o.IdCategory,
                DurationMinutes = o.DurationHours * 60,
                DurationDepth = o.DepthEnd - o.DepthStart
            })
            .ToArray();

        var parentRelationDictionary = wellOperationCategoryRepository.Get(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;
    }

    public async Task<int> InsertRangeAsync(IEnumerable<WellOperationDto> dtos,
        bool deleteBeforeInsert,
        CancellationToken token)
    {
        EnsureValidWellOperations(dtos);

        if (!deleteBeforeInsert)
            return await InsertRangeAsync(dtos, token);

        var idType = dtos.First().IdType;
        var idWell = dtos.First().IdWell;

        var existingOperationIds = await dbContext.WellOperations
            .Where(e => e.IdWell == idWell && e.IdType == idType)
            .Select(e => e.Id)
            .ToArrayAsync(token);

        await DeleteRangeAsync(existingOperationIds, token);

        return await InsertRangeAsync(dtos, token);
    }

    public override Task<int> UpdateRangeAsync(IEnumerable<WellOperationDto> dtos, CancellationToken token)
    {
        EnsureValidWellOperations(dtos);

        return base.UpdateRangeAsync(dtos, token);
    }

    private static void EnsureValidWellOperations(IEnumerable<WellOperationDto> dtos)
    {
        if (dtos.GroupBy(d => d.IdType).Count() > 1)
            throw new ArgumentInvalidException(nameof(dtos), "Все операции должны быть одного типа");

        if (dtos.GroupBy(d => d.IdType).Count() > 1)
            throw new ArgumentInvalidException(nameof(dtos), "Все операции должны принадлежать одной скважине");
    }

    private async Task<IEnumerable<WellOperation>> GetByIdsWells(IEnumerable<int> idsWells, CancellationToken token)
    {
        var query = GetQuery()
            .Where(e => idsWells.Contains(e.IdWell))
            .OrderBy(e => e.DateStart);
        var entities = await query.ToArrayAsync(token);
        return entities;
    }

    private async Task<(IEnumerable<WellOperationDto> items, int count)> GetPrivateAsync(WellOperationRequest request, CancellationToken token)
    {
        var skip = request.Skip ?? 0;
        var take = request.Take ?? 32;

        /*
			каунт = сумма всех фильтеред1
			for{
			все записи по скважине и типу план/факт = wellOperationswithType
			из wellOperationswithType выбираем первую операцию
			из wellOperationswithType все НПВ
			к wellOperationswithType применяем оставшиеся фильтры из buildquery = фильтеред1
			к фильтеред1 применить скип тэйк = фильтеред2
			фильтеред2 конвертировать в дто и рассчитать дэй и время нпв
			...
			}
			*/

        var entities = await GetByIdsWells(request.IdsWell, token);
        var entitiesByWellAndType = entities
            .GroupBy(e => new { e.IdWell, e.IdType })
            .Select(grp => grp.ToArray());

        var result = new List<WellOperationDto>();
        foreach (var wellOperationswithType in entitiesByWellAndType)
        {
            var firstWellOperation = wellOperationswithType
                .OrderBy(e => e.DateStart)
                .FirstOrDefault();

            //НПВ
            var operationsWithNpt = wellOperationswithType
                .Where(o => WellOperationCategory.NonProductiveTimeSubIds.Contains(o.IdCategory));

            var filteredWellOperations = FilterOperations(wellOperationswithType, request);

            var filteredWellOperationsPart = filteredWellOperations
                .Skip(skip)
                .Take(take);

            var dtos = filteredWellOperationsPart
                .Select(o => ConvertWithDrillingDaysAndNpvHours(o, firstWellOperation, operationsWithNpt, token));
            result.AddRange(dtos);
        }

        return (result, entities.Count());
    }

    private IEnumerable<WellOperation> FilterOperations(IEnumerable<WellOperation> entities, WellOperationRequest request)
    {
        if (request.OperationType.HasValue)
            entities = entities.Where(e => e.IdType == request.OperationType.Value);
        if (request.SectionTypeIds?.Any() is true)
            entities = entities.Where(e => request.SectionTypeIds.Contains(e.IdWellSectionType));
        if (request.OperationCategoryIds?.Any() is true)
            entities = entities.Where(e => request.OperationCategoryIds.Contains(e.IdCategory));
        if (request.GeDepth.HasValue)
            entities = entities.Where(e => e.DepthEnd >= request.GeDepth.Value);
        if (request.LeDepth.HasValue)
            entities = entities.Where(e => e.DepthEnd <= request.LeDepth.Value);

        if (request.GeDate.HasValue)
        {
            var geDateUtc = request.GeDate.Value.UtcDateTime;
            entities = entities.Where(e => e.DateStart >= geDateUtc);
        }

        if (request.LeDate.HasValue)
        {
            var leDateUtc = request.LeDate.Value.UtcDateTime;
            entities = entities.Where(e => e.DateStart <= leDateUtc);
        }
        if (request.SortFields?.Any() is true)
            entities = entities.AsQueryable().SortBy(request.SortFields);

        return entities;
    }

    private async Task<IEnumerable<WellOperation>> BuildQuery(WellOperationRequest request, CancellationToken token)
    {
        var entities = await GetByIdsWells(request.IdsWell, token);

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

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

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

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

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

        if (request.GeDate.HasValue)
        {
            var geDateUtc = request.GeDate.Value.UtcDateTime;
            entities = entities.Where(e => e.DateStart >= geDateUtc);
        }

        if (request.LeDate.HasValue)
        {
            var leDateUtc = request.LeDate.Value.UtcDateTime;
            entities = entities.Where(e => e.DateStart <= leDateUtc);
        }

        if (request.SortFields?.Any() is true)
            entities = entities.AsQueryable().SortBy(request.SortFields);

        return entities;
    }

    public async Task<IEnumerable<SectionByOperationsDto>> GetSectionsAsync(IEnumerable<int> idsWells, CancellationToken token)
    {
        const string keyCacheSections = "OperationsBySectionSummarties";

        var cache = await memoryCache.GetOrCreateAsync(keyCacheSections, async (entry) =>
        {
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30);

            var query = dbContext.Set<WellOperation>()
                .GroupBy(operation => new
                {
                    operation.IdWell,
                    operation.IdType,
                    operation.IdWellSectionType,
                    operation.WellSectionType.Caption,
                })
                .Select(group => new
                {
                    group.Key.IdWell,
                    group.Key.IdType,
                    group.Key.IdWellSectionType,
                    group.Key.Caption,

                    First = group
                        .OrderBy(operation => operation.DateStart)
                        .Select(operation => new
                        {
                            operation.DateStart,
                            operation.DepthStart,
                        })
                        .First(),

                    Last = group
                        .OrderByDescending(operation => operation.DateStart)
                        .Select(operation => new
                        {
                            operation.DateStart,
                            operation.DurationHours,
                            operation.DepthEnd,
                        })
                        .First(),
                })
                .Where(s => idsWells.Contains(s.IdWell));
            var dbData = await query.ToArrayAsync(token);
            var sections = dbData.Select(
                    item => new SectionByOperationsDto
                    {
                        IdWell = item.IdWell,
                        IdType = item.IdType,
                        IdWellSectionType = item.IdWellSectionType,

                        Caption = item.Caption,

                        DateStart = item.First.DateStart,
                        DepthStart = item.First.DepthStart,

                        DateEnd = item.Last.DateStart.AddHours(item.Last.DurationHours),
                        DepthEnd = item.Last.DepthEnd,
                    })
                .ToArray()
                .AsEnumerable();

            entry.Value = sections;
            return sections;
        });

        return cache;
    }

    public async Task<DatesRangeDto?> GetDatesRangeAsync(int idWell, int idType, CancellationToken cancellationToken)
    {
        var query = dbContext.WellOperations.Where(o => o.IdWell == idWell && o.IdType == idType);

        if (!await query.AnyAsync(cancellationToken))
            return null;

        var timeZoneOffset = wellService.GetTimezone(idWell).Offset;

        var minDate = await query.MinAsync(o => o.DateStart, cancellationToken);
        var maxDate = await query.MaxAsync(o => o.DateStart, cancellationToken);

        return new DatesRangeDto
        {
            From = minDate.ToOffset(timeZoneOffset),
            To = maxDate.ToOffset(timeZoneOffset)
        };
    }

    private WellOperationDto ConvertWithDrillingDaysAndNpvHours(
        WellOperation entity, 
        WellOperation firstOperation,
        IEnumerable<WellOperation> wellOperationsWithNtp,
        CancellationToken token)
    {
        var dto = Convert(entity);
        dto.Day = (entity.DateStart - firstOperation.DateStart).TotalDays;
        dto.NptHours = wellOperationsWithNtp
            .Where(o => o.DateStart <= entity.DateStart)
            .Sum(e => e.DurationHours);

        return dto;
    }

    protected override WellOperation Convert(WellOperationDto src)
    {
        var entity = src.Adapt<WellOperation>();
        entity.DateStart = src.DateStart.UtcDateTime;
        return entity;
    }

    protected override WellOperationDto Convert(WellOperation src)
    {
        //TODO: пока такое получение TimeZone скважины, нужно исправить на Lazy
        //Хоть мы и тянем данные из кэша, но от получения TimeZone в этом методе нужно избавиться, пока так
        var timeZoneOffset = wellService.GetTimezone(src.IdWell).Offset;
        var dto = src.Adapt<WellOperationDto>();
        dto.DateStart = src.DateStart.ToOffset(timeZoneOffset);
        dto.LastUpdateDate = src.LastUpdateDate.ToOffset(timeZoneOffset);
        return dto;
    }
}