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<WellOperationBaseDto, 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<int> InsertRangeAsync(IEnumerable<WellOperationBaseDto> 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<WellOperationBaseDto> 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<WellOperationBaseDto> 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), "Все операции должны принадлежать одной скважине");
    }

    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 async Task<(WellOperationBaseDto First, WellOperationBaseDto Last)?> GetFirstAndLastAsync(int idWell, int idType, CancellationToken token)
    {
        var cachedDictionary = await memoryCache.GetOrCreateAsync(cacheKeyWellOperations, async (entry) =>
        {
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
            var query = dbContext.Set<WellOperation>()
                .Where(o => o.IdType == idType)
                .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 = await query.ToArrayAsync(token);

            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(WellOperationBaseDto src)
    {
        var entity = src.Adapt<WellOperation>();
        entity.DateStart = src.DateStart.UtcDateTime;
        return entity;
    }

    private WellOperationBaseDto Convert(WellOperation src, TimeSpan timezoneOffset)
    {
        var dto = src.Adapt<WellOperationBaseDto>();
        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;
    }

    public async Task<IEnumerable<WellOperationBaseDto>> GetAll(WellOperationRequest request, CancellationToken token)
    {
        var timezoneOffsetDictionary = new Dictionary<int, TimeSpan>();
        foreach (var idWell in request.IdsWell)
        {
            var offset = wellService.GetTimezone(idWell).Offset;
            timezoneOffsetDictionary.Add(idWell, offset);
        }

        var query = GetQuery()
           .Where(e => request.IdsWell.Contains(e.IdWell))
           .OrderBy(e => e.DateStart)
           .AsQueryable();
        query = FilterByRequest(query, request);

        var entities = await query.ToArrayAsync(token);

        var dtos = entities.Select(o => Convert(o, timezoneOffsetDictionary[o.IdWell]));
        return dtos;
    }

    public async Task<IEnumerable<WellOperationBaseDto>> GetAll(WellOperationRepositoryRequest request, CancellationToken token)
    {
        var timezoneOffsetDictionary = new Dictionary<int, TimeSpan>();
        foreach (var idWell in request.IdsWell)
        {
            var offset = wellService.GetTimezone(idWell).Offset;
            timezoneOffsetDictionary.Add(idWell, offset);
        }

        var query = GetQuery()
           .Where(e => request.IdsWell.Contains(e.IdWell))
           .OrderBy(e => e.DateStart)
           .AsQueryable();
        query = FilterByRequest(query, request);

        var entities = await query.ToArrayAsync(token);

        var dtos = entities.Select(o => Convert(o, timezoneOffsetDictionary[o.IdWell]));
        return dtos;
    }

    public async Task<IEnumerable<WellOperationBaseDto>> GetAll(int idWell, CancellationToken token)
    {
        var offset = wellService.GetTimezone(idWell).Offset;

        var query = GetQuery()
           .Include(o => o.OperationCategory)
           .Include(o => o.WellSectionType)
           .Where(o => o.IdWell == idWell)
           .OrderBy(o => o.DateStart)
           .ThenBy(o => o.DepthEnd);

        var entities = await query.ToArrayAsync(token);

        var dtos = entities.Select(o => Convert(o, offset));
        return dtos;
    }


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

    public static IQueryable<WellOperation> FilterByRequest(IQueryable<WellOperation> entities, WellOperationRepositoryRequest request)
    {
        if (request.OperationType.HasValue)
            entities = entities.Where(e => e.IdType == request.OperationType.Value);
        if (request.LeDepth.HasValue)
            entities = entities.Where(e => e.DepthEnd <= request.LeDepth.Value);
        if (request.LeDate.HasValue)
        {
            var leDateUtc = request.LeDate.Value.UtcDateTime;
            entities = entities.Where(e => e.DateStart <= leDateUtc);
        }
        entities = entities.AsQueryable().OrderBy(e => e.DateStart);

        return entities;
    }
}