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 const string cacheKeyWellOperations = "FirstAndLastFactWellsOperations";
    private readonly IMemoryCache memoryCache;
    private readonly IWellOperationCategoryRepository wellOperationCategoryRepository;
    private readonly IWellService wellService;
    private Lazy<IDictionary<int, WellOperationCategoryDto>> LazyWellCategories { get; }
    private Lazy<IDictionary<int, WellSectionTypeDto>> LazyWellSectionTypes { get; }

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

        LazyWellCategories = new(() => wellOperationCategoryRepository.Get(true, false).ToDictionary(c => c.Id));
        LazyWellSectionTypes = new(() => GetSectionTypes().ToDictionary(c => c.Id));
    }

    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 GetWithDaysAndNpvAsync(request, token);
        return items;
    }

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

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

        var paginationContainer = new PaginationContainer<WellOperationDto>
        {
            Skip = request.Skip!.Value,
            Take = request.Take!.Value,
            Count = count,
            Items = items
        };

        return paginationContainer;
    }

    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
            })
            .ToArrayAsync(token);

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

        var result = 0;

        if (!deleteBeforeInsert)
        {
            result = await InsertRangeAsync(dtos, token);

            if (result > 0)
                memoryCache.Remove(cacheKeyWellOperations);

            return result;
        }
            

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

        result = await InsertRangeAsync(dtos, token);

        if (result > 0)
            memoryCache.Remove(cacheKeyWellOperations);

        return result;

    }

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

        var result = await base.UpdateRangeAsync(dtos, token);

        if (result > 0)
            memoryCache.Remove(cacheKeyWellOperations);

        return result;

    }

    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)> GetWithDaysAndNpvAsync(WellOperationRequest request, CancellationToken token)
    {
        var entities = await GetByIdsWells(request.IdsWell, token);
        var groupedByWellAndType = entities
            .GroupBy(e => new { e.IdWell, e.IdType });

        var result = new List<WellOperationDto>();
        var count = 0;
        foreach (var wellOperationsWithType in groupedByWellAndType)
        {
            var firstWellOperation = wellOperationsWithType
                .OrderBy(e => e.DateStart)
                .FirstOrDefault()!;

            var operationsWithNpt = wellOperationsWithType
                .Where(o => WellOperationCategory.NonProductiveTimeSubIds.Contains(o.IdCategory));

            IEnumerable<WellOperation> filteredWellOperations = FilterByRequest(wellOperationsWithType.AsQueryable(), request);

            count += filteredWellOperations.Count();

            if (request.Skip != null)
                filteredWellOperations = filteredWellOperations.Skip((int)request.Skip);
            if (request.Take != null)
                filteredWellOperations = filteredWellOperations.Take((int)request.Take);

            var dtos = filteredWellOperations
                .Select(entity =>
                {
                    var dto = Convert(entity);
                    dto.Day = (entity.DateStart - firstWellOperation.DateStart).TotalDays;
                    dto.NptHours = operationsWithNpt
                        .Where(o => o.DateStart <= entity.DateStart)
                        .Sum(e => e.DurationHours);
                    return dto;
                });

            result.AddRange(dtos);
        }

        return (result, count);
    }

    private static IQueryable<WellOperation> FilterByRequest(IQueryable<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);
        else
            entities = entities.AsQueryable().OrderBy(e => e.DateStart);

        return entities;
    }

    private IQueryable<WellOperation> BuildQuery(WellOperationRequest request)
    {
        var query = GetQuery()
            .Where(e => request.IdsWell.Contains(e.IdWell))
            .OrderBy(e => e.DateStart)
            .AsQueryable();
        query = FilterByRequest(query, request);

        return query;
    }

    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)
        };
    }

    public (WellOperationDto First, WellOperationDto Last)? GetFirstAndLastFact(int idWell)
    {
        var cachedDictionary = memoryCache.GetOrCreate(cacheKeyWellOperations, (entry) =>
        {
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
            var query = dbContext.Set<WellOperation>()
                .Where(o => o.IdType == WellOperation.IdOperationTypeFact)
                .GroupBy(o => o.IdWell)
                .Select(group => new
                {
                    IdWell = group.Key,
                    FirstFact = group.OrderBy(o => o.DateStart).First(),
                    LastFact = group.OrderBy(o => o.DateStart).Last(),
                });

            var entities = query.ToArray();

            var dictionary = entities.ToDictionary(s => s.IdWell, s => (Convert(s.FirstFact), Convert(s.LastFact)));
            entry.Value = dictionary;

            return dictionary;

        })!;

        var firstAndLast = cachedDictionary.GetValueOrDefault(idWell);
        return firstAndLast;

    }

    public override async Task<int> DeleteAsync(int id, CancellationToken token)
    {
        var result = await base.DeleteAsync(id, token);
        if (result > 0)
            memoryCache.Remove(cacheKeyWellOperations);

        return result;
    }

    public override async Task<int> DeleteRangeAsync(IEnumerable<int> ids, CancellationToken token)
    {
        var result = await base.DeleteRangeAsync(ids, token);
        if (result > 0)
            memoryCache.Remove(cacheKeyWellOperations);

        return result;
    }

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

        dto.OperationCategoryName = LazyWellCategories.Value.TryGetValue(src.IdCategory, out WellOperationCategoryDto? category) ? category.Name : string.Empty;
        dto.WellSectionTypeCaption = LazyWellSectionTypes.Value.TryGetValue(src.IdWellSectionType, out WellSectionTypeDto? sectionType) ? sectionType.Caption : string.Empty;
        return dto;
    }
}