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


/// <summary>
/// репозиторий операций по скважине
/// </summary>
public class WellOperationRepository : IWellOperationRepository
{
    private const string KeyCacheSections = "OperationsBySectionSummarties";
    private readonly IAsbCloudDbContext db;
    private readonly IMemoryCache memoryCache;
    private readonly IWellService wellService;

    public WellOperationRepository(IAsbCloudDbContext db, IMemoryCache memoryCache, IWellService wellService)
    {
        this.db = db;
        this.memoryCache = memoryCache;
        this.wellService = wellService;
    }

    /// <inheritdoc/>
    public IEnumerable<WellOperationCategoryDto> GetCategories(bool includeParents)
    {
        var categories = memoryCache
            .GetOrCreateBasic(db.Set<WellOperationCategory>());

        if (!includeParents)
        {
            var parentIds = categories
                .Select(o => o.IdParent)
                .Distinct();

            categories = categories
                .Where(o => !parentIds.Contains(o.Id));
        }

        var result = categories
            .OrderBy(o => o.Name)                
            .Adapt<IEnumerable<WellOperationCategoryDto>>();

        return result;
    }

    /// <inheritdoc/>
    public IEnumerable<WellSectionTypeDto> GetSectionTypes() =>
        memoryCache
           .GetOrCreateBasic(db.Set<WellSectionType>())
           .OrderBy(s => s.Order)
           .Select(s => s.Adapt<WellSectionTypeDto>());

    public async Task<WellOperationPlanDto> GetOperationsPlanAsync(int idWell, DateTime? currentDate, CancellationToken token)
    {
        var timezone = wellService.GetTimezone(idWell);
        var request = new WellOperationRequest()
        {
            IdWell = idWell,
            OperationType = WellOperation.IdOperationTypePlan,
        };

        var entities = await BuildQuery(request)
            .AsNoTracking()
            .ToArrayAsync(token)
            .ConfigureAwait(false);

        var dateLastAssosiatedPlanOperation = await GetDateLastAssosiatedPlanOperationAsync(idWell, currentDate, timezone.Hours, token);

        var result = new WellOperationPlanDto()
        {
            WellOperationsPlan = entities,
            DateLastAssosiatedPlanOperation = dateLastAssosiatedPlanOperation
        };

        return result;
    }

    private async Task<DateTime?> GetDateLastAssosiatedPlanOperationAsync(
        int idWell, 
        DateTime? lessThenDate, 
        double timeZoneHours, 
        CancellationToken token)
    {
        if (lessThenDate is null)            
            return null;
        
        var currentDateOffset = lessThenDate.Value.ToUtcDateTimeOffset(timeZoneHours);
        var timeZoneOffset = TimeSpan.FromHours(timeZoneHours);

        var lastFactOperation = await db.WellOperations
            .Where(o => o.IdWell == idWell)
            .Where(o => o.IdType == WellOperation.IdOperationTypeFact)
            .Where(o => o.IdPlan != null)
            .Where(o => o.DateStart < currentDateOffset)
            .Include(x => x.OperationPlan)
            .OrderByDescending(x => x.DateStart)
            .FirstOrDefaultAsync(token)
            .ConfigureAwait(false);

        if (lastFactOperation is not null)
            return DateTime.SpecifyKind(lastFactOperation.OperationPlan.DateStart.UtcDateTime + timeZoneOffset, DateTimeKind.Unspecified);
        return null;
    }

    /// <inheritdoc/>
    public async Task<IEnumerable<SectionByOperationsDto>> GetSectionsAsync(IEnumerable<int> idsWells, CancellationToken token)
    {
        var cache = await memoryCache.GetOrCreateAsync(KeyCacheSections, async (entry) =>
        {
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30);

            var query = db.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(),
                });
            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;
        });

        var sections = cache.Where(s => idsWells.Contains(s.IdWell));
        return sections;
    }

    /// <inheritdoc/>
    public DateTimeOffset? FirstOperationDate(int idWell)
    {
        var sections = GetSectionsAsync(new[] { idWell }, CancellationToken.None).Result;
        var first = sections.FirstOrDefault(section => section.IdType == WellOperation.IdOperationTypeFact)
            ?? sections.FirstOrDefault(section => section.IdType == WellOperation.IdOperationTypePlan);

        return first?.DateStart;
    }

    /// <inheritdoc/>
    public async Task<IEnumerable<WellOperationDto>> GetAsync(
        WellOperationRequest request,
        CancellationToken token)
    {
        var query = BuildQuery(request)
            .AsNoTracking();
        var result = await query.ToArrayAsync(token);
        return result;
    }

    /// <inheritdoc/>
    public async Task<PaginationContainer<WellOperationDto>> GetPageAsync(
        WellOperationRequest request,
        CancellationToken token)
    {
        var query = BuildQuery(request)
            .AsNoTracking();

        var result = new PaginationContainer<WellOperationDto>
        {
            Skip = request.Skip ?? 0,
            Take = request.Take ?? 32,
            Count = await query.CountAsync(token).ConfigureAwait(false),
        };

        query = query
            .Skip(result.Skip)
            .Take(result.Take);

        result.Items = await query.ToArrayAsync(token);
        return result;
    }

    /// <inheritdoc/>
    public async Task<WellOperationDto?> GetOrDefaultAsync(int id,
        CancellationToken token)
    {
        var entity = await db.WellOperations
            .Include(s => s.WellSectionType)
            .Include(s => s.OperationCategory)
            .FirstOrDefaultAsync(e => e.Id == id, token)
            .ConfigureAwait(false);

        if (entity is null)
            return null;

        var timezone = wellService.GetTimezone(entity.IdWell);

        var dto = entity.Adapt<WellOperationDto>();
        dto.WellSectionTypeName = entity.WellSectionType.Caption;
        dto.DateStart = entity.DateStart.ToRemoteDateTime(timezone.Hours);
        dto.CategoryName = entity.OperationCategory.Name;
        return dto;
    }

    /// <inheritdoc/>
    public async Task<IEnumerable<WellGroupOpertionDto>> GetGroupOperationsStatAsync(
        WellOperationRequest request,
        CancellationToken token)
    {
        // TODO: Rename controller method
        request.OperationType = WellOperation.IdOperationTypeFact;
        var query = BuildQuery(request);
        var entities = await query
            .Select(o => new
            {
                o.IdCategory,
                DurationMinutes = o.DurationHours * 60,
                DurationDepth = o.DepthEnd - o.DepthStart
            })
            .ToListAsync(token);
        var parentRelationDictionary = GetCategories(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;
    }

    /// <inheritdoc/>
    public async Task<int> InsertRangeAsync(
        IEnumerable<WellOperationDto> wellOperationDtos,
        CancellationToken token)
    {
        var firstOperation = wellOperationDtos
            .FirstOrDefault();
        if (firstOperation is null)
            return 0;

        var idWell = firstOperation.IdWell;

        var timezone = wellService.GetTimezone(idWell);
        foreach (var dto in wellOperationDtos)
        {
            var entity = dto.Adapt<WellOperation>();
            entity.Id = default;
            entity.DateStart = dto.DateStart.ToUtcDateTimeOffset(timezone.Hours);
            entity.IdWell = idWell;
            db.WellOperations.Add(entity);
        }

        var result = await db.SaveChangesAsync(token);
        if (result > 0)
            memoryCache.Remove(KeyCacheSections);
        return result;

    }

    /// <inheritdoc/>
    public async Task<int> UpdateAsync(
        WellOperationDto dto, CancellationToken token)
    {
        var timezone = wellService.GetTimezone(dto.IdWell);
        var entity = dto.Adapt<WellOperation>();
        entity.DateStart = dto.DateStart.ToUtcDateTimeOffset(timezone.Hours);
        db.WellOperations.Update(entity);

        var result = await db.SaveChangesAsync(token);
        if (result > 0)
            memoryCache.Remove(KeyCacheSections);
        return result;
    }

    /// <inheritdoc/>
    public async Task<int> DeleteAsync(IEnumerable<int> ids,
        CancellationToken token)
    {
        var query = db.WellOperations.Where(e => ids.Contains(e.Id));
        db.WellOperations.RemoveRange(query);

        var result = await db.SaveChangesAsync(token);
        if (result > 0)
            memoryCache.Remove(KeyCacheSections);
        return result;
    }

    /// <summary>
    /// В результате попрежнему требуется конвертировать дату
    /// </summary>
    /// <param name="request"></param>
    /// <returns></returns>
    private IQueryable<WellOperationDto> BuildQuery(WellOperationRequest request)
    {
        var timezone = wellService.GetTimezone(request.IdWell);
        var timeZoneOffset = TimeSpan.FromHours(timezone.Hours);

        var query = db.WellOperations
            .Include(s => s.WellSectionType)
            .Include(s => s.OperationCategory)
            .Where(o => o.IdWell == request.IdWell);


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

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

        if (request.OperationCategoryIds?.Any() == 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 geDateOffset = request.GeDate.Value.ToUtcDateTimeOffset(timezone.Hours);
            query = query.Where(e => e.DateStart >= geDateOffset);
        }

        if (request.LtDate.HasValue)
        {
            var ltDateOffset = request.LtDate.Value.ToUtcDateTimeOffset(timezone.Hours);
            query = query.Where(e => e.DateStart < ltDateOffset);
        }

        var currentWellOperations = db.WellOperations
            .Where(subOp => subOp.IdWell == request.IdWell);

        var wellOperationsWithCategoryNPT = currentWellOperations
            .Where(subOp => subOp.IdType == 1)
            .Where(subOp => WellOperationCategory.NonProductiveTimeSubIds.Contains(subOp.IdCategory));

        var result = query.Select(o => new WellOperationDto
        {
            Id = o.Id,
            IdPlan = o.IdPlan,
            IdType = o.IdType,
            IdWell = o.IdWell,
            IdWellSectionType = o.IdWellSectionType,
            IdCategory = o.IdCategory,
            IdParentCategory = o.OperationCategory.IdParent,

            CategoryName = o.OperationCategory.Name,
            WellSectionTypeName = o.WellSectionType.Caption,

            DateStart = DateTime.SpecifyKind(o.DateStart.UtcDateTime + timeZoneOffset, DateTimeKind.Unspecified),
            DepthStart = o.DepthStart,
            DepthEnd = o.DepthEnd,
            DurationHours = o.DurationHours,
            CategoryInfo = o.CategoryInfo,
            Comment = o.Comment,

            NptHours = wellOperationsWithCategoryNPT
                     .Where(subOp => subOp.DateStart <= o.DateStart)
                     .Select(subOp => subOp.DurationHours)
                     .Sum(),

            Day = (o.DateStart - currentWellOperations
                     .Where(subOp => subOp.IdType == o.IdType)
                     .Where(subOp => subOp.DateStart <= o.DateStart)
                     .Min(subOp => subOp.DateStart))
                     .TotalDays,
            IdUser = o.IdUser,
            LastUpdateDate = o.LastUpdateDate.ToOffset(TimeSpan.FromHours(timezone.Hours))
        });

        if (request.SortFields?.Any() == true)
        {
            result = result.SortBy(request.SortFields);
        }
        else
        {
            result = result
                .OrderBy(e => e.DateStart)
                .ThenBy(e => e.DepthEnd)
                .ThenBy(e => e.Id);
        };

        return result;
    }
}