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.ComponentModel.DataAnnotations;
using System.Diagnostics;
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;
    private readonly IWellOperationCategoryRepository wellOperationCategoryRepository;

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

    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 dtos = await BuildQuery(request)
            .AsNoTracking()
            .ToArrayAsync(token);

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

        var result = new WellOperationPlanDto()
        {
            WellOperationsPlan = dtos.Select(Convert),
            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;
    }

    public async Task<DatesRangeDto?> GetDatesRangeAsync(int idWell, int idType, CancellationToken cancellationToken)
    {
        var timezone = wellService.GetTimezone(idWell);

        var query = db.WellOperations.Where(o => o.IdWell == idWell && o.IdType == idType);

        if (!await query.AnyAsync(cancellationToken))
            return null;

        var minDate = await query.MinAsync(o => o.DateStart, cancellationToken);
        var maxDate = await query.MaxAsync(o => o.DateStart, cancellationToken);

        return new DatesRangeDto
        {
            From = minDate.ToOffset(timezone.Offset),
            To = maxDate.ToOffset(timezone.Offset)
        };
    }

    /// <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 dtos = await query.ToArrayAsync(token);

            return dtos.Select(Convert);     
    }

    public async Task<IEnumerable<WellOperationDataDto>> GetAsync(
       WellsOperationRequest request,
       CancellationToken token)
    {
        var query = BuildQuery(request)
           .AsNoTracking();

        var dtos = await query.ToArrayAsync(token);
        return dtos;
    }

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

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

        var dtos = await query.ToArrayAsync(token);

        result.Items = dtos.Select(Convert);

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

    /// <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.DateTime.ToUtcDateTimeOffset(timezone.Hours);
            entity.IdWell = idWell;
            entity.LastUpdateDate = DateTimeOffset.UtcNow;
            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.DateTime.ToUtcDateTimeOffset(timezone.Hours);
        entity.LastUpdateDate = DateTimeOffset.UtcNow;
        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>
    /// <param name="token"></param>
    /// <returns></returns>
    private IQueryable<WellOperationDto> BuildQuery(WellOperationRequest request)
    {
        var timezone = wellService.GetTimezone(request.IdWell);
        var timeZoneOffset = 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.ToUniversalTime();
            query = query.Where(e => e.DateStart >= geDateOffset);
        }

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

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

        // TODO: Вынести query.Select из метода BuildQuery
        var dtos = 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 = o.DateStart,
            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,
        });

        if (request.SortFields?.Any() == true)
        {
            dtos = dtos.SortBy(request.SortFields);
        }

        dtos = dtos
            .OrderBy(e => e.DateStart)
            .ThenBy(e => e.DepthEnd)
            .ThenBy(e => e.Id);

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

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

        return dtos.AsNoTracking();
    }

    /// <summary>
    /// Получение данных по запросу
    /// </summary>
    /// <param name="request"></param>
    /// <param name="token"></param>
    /// <returns></returns>
    private IQueryable<WellOperationDataDto> BuildQuery(WellsOperationRequest request)
    {
        var query = db.WellOperations
           .Where(o => request.IdsWell.Contains(o.IdWell))
           .Where(o => request.OperationType == o.IdType);

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

        if (request.OperationCategoryIds?.Any() == true)
            query = query.Where(o => request.OperationCategoryIds.Contains(o.IdCategory));

        // TODO: Вынести query.Select из метода BuildQuery
        var dtos = query.Select(o => new WellOperationDataDto
        {
            DepthStart = o.DepthStart,
            DurationHours = o.DurationHours,
            IdCategory = o.IdCategory,
            IdWell = o.IdWell,
            IdWellSectionType = o.IdWellSectionType,
            OperationCategoryName = o.OperationCategory.Name,
            WellSectionTypeCaption = o.WellSectionType.Caption,
        });

        if (request.SortFields?.Any() == true)
        {
            dtos = dtos.SortBy(request.SortFields);
        }

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

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

        return dtos.AsNoTracking();
    }

    private WellOperationDto Convert(WellOperationDto dto)
    {
        var timezone = wellService.GetTimezone(dto.IdWell);
        var timezoneOffset = TimeSpan.FromHours(timezone.Hours);

        var dtoWithRemoteDateTime = dto.Adapt<WellOperationDto>();

        dtoWithRemoteDateTime.DateStart = dto.DateStart.ToOffset(TimeSpan.FromHours(timezoneOffset.Hours));
        dtoWithRemoteDateTime.LastUpdateDate = dto.LastUpdateDate?.ToOffset(TimeSpan.FromHours(timezoneOffset.Hours));

        return dtoWithRemoteDateTime;
    }

    public async Task<int> RemoveDuplicates(Action<string, double?> onProgressCallback, CancellationToken token)
    {
        IQueryable<WellOperation> dbset = db.Set<WellOperation>();
        var query = dbset
            .GroupBy(o => new { o.IdWell, o.IdType })
            .Select(g => new { g.Key.IdWell, g.Key.IdType });

        var groups = await query
            .ToArrayAsync(token);

        var count = groups.Count();
        var i = 0;
        var totalRemoved = 0;
        var total = 0;
        foreach (var group in groups)
        {
            var result = await RemoveDuplicatesInGroup(group.IdWell, group.IdType, token);
            totalRemoved += result.removed;
            total += result.total;
            var percent = i++ / count;
            var message = $"RemoveDuplicates [{i} of {count}] wellId: {group.IdWell}, opType: {group.IdType}, affected: {result.removed} of {result.total}";
            onProgressCallback?.Invoke(message, percent);
            Trace.TraceInformation(message);
        }
        var messageDone = $"RemoveDuplicates done [{i} of {count}] totalAffected: {totalRemoved} of {total}";
        Trace.TraceInformation(messageDone);
        onProgressCallback?.Invoke(messageDone, 1);
        return totalRemoved;
    }

    private async Task<(int removed, int total)> RemoveDuplicatesInGroup(int idWell, int idType, CancellationToken token)
    {
        var dbset = db.Set<WellOperation>();
        var entities = await dbset
            .Where(o => o.IdWell == idWell && o.IdType == idType)
            .OrderBy(o => o.DateStart)
            .ToListAsync(token);

        using var entitiesEnumerator = entities.GetEnumerator();

        if (!entitiesEnumerator.MoveNext())
            return (0, 0);

        var preEntity = entitiesEnumerator.Current;
        while (entitiesEnumerator.MoveNext())
        {
            var entity = entitiesEnumerator.Current;
            if (preEntity.IsSame(entity))
                dbset.Remove(entity);
            else
                preEntity = entity;
        }
        var removed = await db.SaveChangesAsync(token);
        return (removed, entities.Count);
    }

    public async Task<int> TrimOverlapping(DateTimeOffset? geDate, DateTimeOffset leDate, Action<string, double?> onProgressCallback, CancellationToken token)
    {
        var leDateUtc = leDate.ToUniversalTime();
        IQueryable<WellOperation> query = db.Set<WellOperation>();
        if (geDate.HasValue)
        {
            var geDateUtc = geDate.Value.ToUniversalTime();
            query = query.Where(e => e.DateStart >= geDateUtc);
        }

        var groups = await query
            .GroupBy(o => new { o.IdWell, o.IdType })
            .Select(g => new{ 
                MaxDate = g.Max(o => o.DateStart),
                g.Key.IdWell,
                g.Key.IdType,
            })
            .Where(g => g.MaxDate <= leDateUtc)
            .ToArrayAsync(token);

        var count = groups.Count();
        var i = 0;
        (int takeover, int trimmed,int total) totalResult = (0, 0, 0);
        foreach (var group in groups)
        {
            var result = await TrimOverlapping(group.IdWell, group.IdType, token);
            totalResult.takeover += result.takeover;
            totalResult.trimmed += result.trimmed;
            totalResult.total += result.total;
            var percent = i++ / count;
            var message = $"TrimOverlapping [{i} of {count}] wellId: {group.IdWell}, opType: {group.IdType}, takeover:{result.takeover}, trimmed:{result.trimmed}, of {result.total}";
            onProgressCallback?.Invoke(message, percent);
            Trace.TraceInformation(message);
        }
        var messageDone = $"TrimOverlapping done [{i} of {count}] total takeover:{totalResult.takeover}, total trimmed:{totalResult.trimmed} of {totalResult.total}";
        Trace.TraceInformation(messageDone);
        onProgressCallback?.Invoke(messageDone, 1);
        return totalResult.takeover + totalResult.trimmed;
    }

    private async Task<(int takeover, int trimmed, int total)> TrimOverlapping(int idWell, int idType, CancellationToken token)
    {
        var dbset = db.Set<WellOperation>();
        var query = dbset
            .Where(o => o.IdWell == idWell)
            .Where(o => o.IdType == idType)
            .OrderBy(o => o.DateStart)
            .ThenBy(o => o.DepthStart);

        var entities = await query
            .ToListAsync(token);

        using var entitiesEnumerator = entities.GetEnumerator();

        if (!entitiesEnumerator.MoveNext())
            return (0, 0, 0);

        int takeover = 0;
        int trimmed = 0;
        var preEntity = entitiesEnumerator.Current;
        while (entitiesEnumerator.MoveNext())
        {
            var entity = entitiesEnumerator.Current;
            var preDepth = preEntity.DepthEnd;

            if (preEntity.DepthEnd >= entity.DepthEnd)
            {
                dbset.Remove(entity);
                takeover++;
                continue;
            }

            if (preEntity.DepthEnd > entity.DepthStart)
            {
                entity.DepthStart = preEntity.DepthEnd;
                trimmed++;
            }

            var preDate = preEntity.DateStart.AddHours(preEntity.DurationHours);

            if (preDate >= entity.DateStart.AddHours(entity.DurationHours))
            {
                dbset.Remove(entity);
                takeover++;
                continue;
            }

            if (preDate > entity.DateStart)
            {
                var entityDateEnd = entity.DateStart.AddHours(entity.DurationHours);
                entity.DateStart = preDate;
                entity.DurationHours = (entityDateEnd - entity.DateStart).TotalHours;
                trimmed++;
            }

            preEntity = entity;
        }
        var affected = await db.SaveChangesAsync(token);
        return (takeover, trimmed, entities.Count);
    }
}