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 keyCacheTemplate = "OperationsBySectionSummaries_{0}";
    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)
    {
        var result = new List<SectionByOperationsDto>();
        var notFoundIds = new List<int>();

        foreach (var idWell in idsWells)
        {
            var cacheKey = string.Format(keyCacheTemplate, idWell);
            if (memoryCache.TryGetValue<IEnumerable<SectionByOperationsDto>>(cacheKey, out var section))
            {
                result.AddRange(section!);
            }
            else
            {
                notFoundIds.Add(idWell);
            }
        }

        if (notFoundIds.Count != 0)
        {
            var query = dbContext.Set<WellOperation>()
                .Where(operation => notFoundIds.Contains( operation.IdWell))
                .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(),
                });

            var entities = await query.ToArrayAsync(token);
            var dtos = entities.Select(
                    entity => new SectionByOperationsDto
                    {
                        IdWell = entity.IdWell,
                        IdType = entity.IdType,
                        IdWellSectionType = entity.IdWellSectionType,
                        Caption = entity.Caption,
                        DateStart = entity.First.DateStart,
                        DepthStart = entity.First.DepthStart,
                        DateEnd = entity.Last.DateStart.AddHours(entity.Last.DurationHours),
                        DepthEnd = entity.Last.DepthEnd,
                    })
                .ToList();

            result.AddRange(dtos);

            var groupedByWellDtos = dtos
                .GroupBy(dto => dto.IdWell);

            foreach (var group in groupedByWellDtos)
            {
                var cacheKey = string.Format(keyCacheTemplate, group.Key);
                memoryCache.Set(cacheKey, group.AsEnumerable(), TimeSpan.FromMinutes(30));
            }
        }

        return result;
    }

    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 (WellOperationBaseDto First, WellOperationBaseDto 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(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;
    }
}