using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
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;

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 query = BuildQuery(request);

		if (request.Skip.HasValue)
			query = query.Skip(request.Skip.Value);

		if (request.Take.HasValue)
			query = query.Take(request.Take.Value);

		var entities = await query.AsNoTracking()
			.ToArrayAsync(token);

		return await ConvertWithDrillingDaysAndNpvHoursAsync(entities, token);
	}

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

		var query = BuildQuery(request);

		var entities = await query.Skip(skip)
			.Take(take)
			.AsNoTracking()
			.ToArrayAsync(token);

		var paginationContainer = new PaginationContainer<WellOperationDto>
		{
			Skip = skip,
			Take = take,
			Count = await query.CountAsync(token),
			Items = await ConvertWithDrillingDaysAndNpvHoursAsync(entities, token)
		};

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

		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 IQueryable<WellOperation> BuildQuery(WellOperationRequest request)
	{
		var query = GetQuery()
			.Where(e => request.IdsWell != null && request.IdsWell.Contains(e.IdWell))
			.OrderBy(e => e.DateStart)
			.AsQueryable();

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

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

		if (request.OperationCategoryIds?.Any() is 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 geDateUtc = request.GeDate.Value.UtcDateTime;
			query = query.Where(e => e.DateStart >= geDateUtc);
		}

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

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

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

	private async Task<IEnumerable<WellOperationDto>> ConvertWithDrillingDaysAndNpvHoursAsync(IEnumerable<WellOperation> entities,
		CancellationToken token)
	{
		var idsWell = entities.Select(e => e.IdWell).Distinct();

		var currentWellOperations = GetQuery()
			.Where(entity => idsWell.Contains(entity.IdWell));

		var dateFirstDrillingOperationByIdWell = await currentWellOperations
			.Where(entity => entity.IdType == WellOperation.IdOperationTypeFact)
			.GroupBy(entity => entity.IdWell)
			.ToDictionaryAsync(g => g.Key, g => g.Min(o => o.DateStart), token);

		var operationsWithNptByIdWell = await currentWellOperations.Where(entity =>
				entity.IdType == WellOperation.IdOperationTypeFact &&
				WellOperationCategory.NonProductiveTimeSubIds.Contains(entity.IdCategory))
			.GroupBy(entity => entity.IdWell)
			.ToDictionaryAsync(g => g.Key, g => g.Select(o => o), token);

		var dtos = entities.Select(entity =>
		{
			var dto = Convert(entity);

			if (dateFirstDrillingOperationByIdWell.TryGetValue(entity.IdWell, out var dateFirstDrillingOperation))
				dto.Day = (entity.DateStart - dateFirstDrillingOperation).TotalDays;

			if (operationsWithNptByIdWell.TryGetValue(entity.IdWell, out var wellOperationsWithNtp))
				dto.NptHours = wellOperationsWithNtp
					.Where(o => o.DateStart <= entity.DateStart)
					.Sum(e => e.DurationHours);

			return dto;
		});

		return dtos;
	}

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