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

    public class OperationsStatService : IOperationsStatService
    {
        private readonly IAsbCloudDbContext db;
        private readonly IMemoryCache memoryCache;
        private readonly IWellService wellService;

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

        public async Task<StatClusterDto?> GetOrDefaultStatClusterAsync(int idCluster, int idCompany, CancellationToken token)
        {
            var cluster = (await memoryCache
                .GetOrCreateBasicAsync(db.Set<Cluster>(), token))
                .FirstOrDefault(c => c.Id == idCluster);

            if (cluster is null)
                return null;

            var allWellsByCompany = await wellService.GetAsync(new() { IdCompany = idCompany }, token).ConfigureAwait(false);

            var idWellsByCompany = allWellsByCompany.Select(w => w.Id).Distinct();

            var wells = await db.Wells
                .Include(w => w.WellOperations)
                .Where(o => o.IdCluster == idCluster)
                .Where(w => idWellsByCompany.Contains(w.Id))
                .Select(w => w.Id)
                .ToListAsync(token);

            var statsWells = await GetWellsStatAsync(wells, token).ConfigureAwait(false);

            var statClusterDto = new StatClusterDto
            {
                Id = idCluster,
                Caption = cluster.Caption,
                StatsWells = statsWells,
            };
            return statClusterDto;
        }

        public async Task<IEnumerable<StatWellDto>> GetWellsStatAsync(IEnumerable<int> idWells, CancellationToken token)
        {
            var wells = await db.Wells
                .Include(w => w.WellOperations)
                .Where(w => idWells.Contains(w.Id))
                .AsNoTracking()
                .ToListAsync(token);

            var statsWells = new List<StatWellDto>(wells.Count);

            foreach (var well in wells)
            {
                var statWellDto = await CalcWellStatAsync(well, token);
                statsWells.Add(statWellDto);
            }
            return statsWells;
        }

        public async Task<StatWellDto?> GetOrDefaultWellStatAsync(int idWell,
            CancellationToken token = default)
        {
            var well = await db.Wells
                .Include(w => w.WellOperations)
                .FirstOrDefaultAsync(w => w.Id == idWell, token)
                .ConfigureAwait(false);

            if(well is null)
                return null;

            var statWellDto = await CalcWellStatAsync(well, token);
            return statWellDto;
        }

        public async Task<ClusterRopStatDto?> GetOrDefaultRopStatAsync(int idWell, CancellationToken token)
        {
            var clusterWellsIds = await wellService.GetClusterWellsIdsAsync(idWell, token)
                .ConfigureAwait(false);

            if (clusterWellsIds is null)
                return null;

            var idLastSectionType = await (from o in db.WellOperations
                                           where o.IdWell == idWell &&
                                                 o.IdType == 1
                                           orderby o.DepthStart
                                           select o.IdWellSectionType)
                .LastOrDefaultAsync(token)
                .ConfigureAwait(false);

            if (idLastSectionType == default)
                return null;

            var operations = await (from o in db.WellOperations
                                    where clusterWellsIds.Contains(o.IdWell) &&
                                          o.IdType == 1 &&
                                          o.IdWellSectionType == idLastSectionType
                                    select o)
                .ToListAsync(token)
                .ConfigureAwait(false);

            var statsList = new List<StatOperationsDto>(clusterWellsIds.Count());
            foreach (var clusterWellId in clusterWellsIds)
            {
                var currentWellOps = operations.Where(o => o.IdWell == clusterWellId);
                var timezoneOffsetHours = wellService.GetTimezone(clusterWellId).Hours;
                var stat = CalcStat(currentWellOps, timezoneOffsetHours);
                if(stat is not null)
                    statsList.Add(stat);
            };

            if (!statsList.Any())
                return null;

            var clusterRops = new ClusterRopStatDto()
            {
                RopMax = statsList.Max(s => s.Rop),
                RopAverage = statsList.Average(s => s.Rop)
            };

            return clusterRops;
        }

        private async Task<StatWellDto> CalcWellStatAsync(Well well, CancellationToken token)
        {
            var wellType = (await memoryCache
                .GetOrCreateBasicAsync(db.Set<WellType>(), token))
                .FirstOrDefault(t => t.Id == well.IdWellType);
            var statWellDto = new StatWellDto
            {
                Id = well.Id,
                Caption = well.Caption,
                WellType = wellType?.Caption ?? "",
                IdState = well.IdState,
                State = wellService.GetStateText(well.IdState),
                LastTelemetryDate = wellService.GetLastTelemetryDate(well.Id),
                Companies = await wellService.GetCompaniesAsync(well.Id, token)
            };

            if (well.WellOperations is null)
                return statWellDto;

            var wellOperations = well.WellOperations
                .OrderBy(o => o.DateStart)
                .ThenBy(o => o.DepthEnd);

            if (!wellOperations.Any())
                return statWellDto;

            var timezoneOffsetH = wellService.GetTimezone(well.Id).Hours;
            statWellDto.Sections = CalcSectionsStats(wellOperations, timezoneOffsetH);
            statWellDto.Total = GetStatTotal(wellOperations, well.IdState, timezoneOffsetH);
            statWellDto.TvdLagDays = CalcTvdLagDays(wellOperations);

            return statWellDto;
        }

        private static double CalcTvdLagDays(IOrderedEnumerable<WellOperation> wellOperations)
        {
            var operationsOrdered = wellOperations
                .OrderBy(o => o.DateStart);

            var factOperations = operationsOrdered
                .Where(o => o.IdType == WellOperation.IdOperationTypeFact);

            var lastCorrespondingFactOperation = factOperations
                .LastOrDefault(o => o.IdPlan is not null);
            
            if (lastCorrespondingFactOperation is null)
                return 0d;
            
            var lastCorrespondingPlanOperation = wellOperations
                .FirstOrDefault(o => o.Id == lastCorrespondingFactOperation.IdPlan);

            if (lastCorrespondingPlanOperation is null) 
                return 0d;

            var lastFactOperation = factOperations.Last();

            var remainingPlanOperations = operationsOrdered
                .Where(o => o.IdType == WellOperation.IdOperationTypePlan)
                .Where(o => o.DateStart > lastCorrespondingPlanOperation.DateStart);

            var durationRemain = remainingPlanOperations.Sum(o => o.DurationHours);

            var factEnd = lastFactOperation.DateStart.AddHours(durationRemain + lastFactOperation.DurationHours);
            var planEnd = lastCorrespondingFactOperation.DateStart.AddHours(durationRemain + lastCorrespondingFactOperation.DurationHours);
            var lagDays = (planEnd - factEnd).TotalDays;

            return lagDays;
        }

        private IEnumerable<StatSectionDto> CalcSectionsStats(IEnumerable<WellOperation> operations, double timezoneOffsetH)
        {
            var sectionTypeIds = operations
                .Select(o => o.IdWellSectionType)
                .Distinct();

            var sectionTypes = memoryCache
                .GetOrCreateBasic(db.Set<WellSectionType>())
                .Where(s => sectionTypeIds.Contains(s.Id))
                .ToDictionary(s => s.Id);

            var sections = new List<StatSectionDto>(sectionTypes.Count);
            var operationsPlan = operations.Where(o => o.IdType == WellOperation.IdOperationTypePlan);
            var operationsFact = operations.Where(o => o.IdType == WellOperation.IdOperationTypeFact);

            foreach ((var id, var sectionType) in sectionTypes)
            {
                var section = new StatSectionDto
                {
                    Id = id,
                    Caption = sectionType.Caption,
                    Plan = CalcSectionStat(operationsPlan, id, timezoneOffsetH),
                    Fact = CalcSectionStat(operationsFact, id, timezoneOffsetH),
                };
                sections.Add(section);
            }
            return sections;
        }

        private static PlanFactDto<StatOperationsDto> GetStatTotal(IEnumerable<WellOperation> operations,
            int idWellState, double timezoneOffsetH)
        {
            var operationsPlan = operations.Where(o => o.IdType == WellOperation.IdOperationTypePlan);
            var operationsFact = operations.Where(o => o.IdType == WellOperation.IdOperationTypeFact);
            var factEnd = CalcStat(operationsFact, timezoneOffsetH);
            if (factEnd is not null && idWellState != 2)
                factEnd.End = null;
            var section = new PlanFactDto<StatOperationsDto>
            {
                Plan = CalcStat(operationsPlan, timezoneOffsetH),
                Fact = factEnd,
            };
            return section;
        }

        private static StatOperationsDto? CalcSectionStat(IEnumerable<WellOperation> operations, int idSectionType, double timezoneOffsetHours)
        {
            var sectionOperations = operations
                .Where(o => o.IdWellSectionType == idSectionType)
                .OrderBy(o => o.DateStart)
                .ThenBy(o => o.DepthStart);

            return CalcStat(sectionOperations, timezoneOffsetHours);
        }

        private static StatOperationsDto? CalcStat(IEnumerable<WellOperation> operations, double timezoneOffsetHours)
        {
            if (!operations.Any())
                return null;

            var races = GetCompleteRaces(operations, timezoneOffsetHours);

            var section = new StatOperationsDto
            {
                Start = operations.FirstOrDefault()?.DateStart.ToRemoteDateTime(timezoneOffsetHours),
                End = operations.Max(o => o.DateStart.ToRemoteDateTime(timezoneOffsetHours).AddHours(o.DurationHours)),
                WellDepthStart = operations.Min(o => o.DepthStart),
                WellDepthEnd = operations.Max(o => o.DepthStart),
                Rop = CalcROP(operations),
                RouteSpeed = CalcAvgRaceSpeed(races),
                BhaDownSpeed = CalcBhaDownSpeed(races),
                BhaUpSpeed = CalcBhaUpSpeed(races),
                CasingDownSpeed = CalcCasingDownSpeed(operations),
                NonProductiveHours = operations
                    .Where(o => WellOperationCategory.NonProductiveTimeSubIds.Contains(o.IdCategory))
                    .Sum(o => o.DurationHours),
            };
            return section;
        }

        private static double CalcROP(IEnumerable<WellOperation> operationsProps)
        {
            var drillingOperations = operationsProps.Where(o => WellOperationCategory.MechanicalDrillingSubIds.Contains(o.IdCategory));
            var dDepth = 0d;
            var dHours = 0d;
            foreach (var operation in drillingOperations)
            {
                var deltaDepth = operation.DepthEnd - operation.DepthStart;
                dDepth += deltaDepth;
                dHours += operation.DurationHours;
            }
            return dDepth / (dHours + double.Epsilon);
        }

        private static double CalcCasingDownSpeed(IEnumerable<WellOperation> operationsProps)
        {
            var ops = operationsProps.Where(o => o.IdCategory == WellOperationCategory.IdCasingDown);
            var depth = 0d;
            var dHours = 0d;
            foreach (var operation in ops)
            {
                depth += operation.DepthStart;
                dHours += operation.DurationHours;
            }
            return depth / (dHours + double.Epsilon);
        }

        private static IEnumerable<Race> GetCompleteRaces(IEnumerable<WellOperation> operations, double timezoneOffsetH)
        {
            var races = new List<Race>();
            var iterator = operations
                .OrderBy(o => o.DateStart)
                .GetEnumerator();
            while (iterator.MoveNext())
            {
                if (iterator.Current.IdCategory == WellOperationCategory.IdBhaAssembly)
                {
                    var race = new Race
                    {
                        StartDate = iterator.Current.DateStart.ToRemoteDateTime(timezoneOffsetH).AddHours(iterator.Current.DurationHours),
                        StartWellDepth = iterator.Current.DepthStart,
                        Operations = new List<WellOperation>(10),
                    };
                    while (iterator.MoveNext())
                    {
                        if (iterator.Current.IdCategory == WellOperationCategory.IdEquipmentRepair)
                            race.RepairHours += iterator.Current.DurationHours;

                        if (WellOperationCategory.NonProductiveTimeSubIds.Contains(iterator.Current.IdCategory))
                            race.NonProductiveHours += iterator.Current.DurationHours;

                        if (iterator.Current.IdCategory == WellOperationCategory.IdBhaDisassembly)
                        {
                            race.EndDate = iterator.Current.DateStart.ToRemoteDateTime(timezoneOffsetH);
                            race.EndWellDepth = iterator.Current.DepthStart;
                            races.Add(race);
                            break;
                        }
                        race.Operations.Add(iterator.Current);
                    }
                }
            }
            return races;
        }

        private static double CalcAvgRaceSpeed(IEnumerable<Race> races)
        {
            var dDepth = 0d;
            var dHours = 0d;
            foreach (var race in races)
            {
                dHours += race.DeltaHours - race.NonProductiveHours - race.RepairHours;
                dDepth += race.DeltaDepth;
            }
            return dDepth / (dHours + double.Epsilon);
        }

        private static double CalcBhaDownSpeed(IEnumerable<Race> races)
        {
            var dDepth = 0d;
            var dHours = 0d;
            foreach (Race race in races)
            {
                dDepth += race.StartWellDepth;
                for (var i = 0; i < race.Operations.Count; i++)
                {
                    if (race.Operations[i].IdCategory == WellOperationCategory.IdBhaDown)
                        dHours += race.Operations[i].DurationHours;
                    if (WellOperationCategory.MechanicalDrillingSubIds.Contains(race.Operations[i].IdCategory))
                        break;
                }
            }
            return dDepth / (dHours + double.Epsilon);
        }

        private static double CalcBhaUpSpeed(IEnumerable<Race> races)
        {
            var dDepth = 0d;
            var dHours = 0d;
            foreach (var race in races)
            {
                dDepth += race.EndWellDepth;
                for (var i = race.Operations.Count - 1; i > 0; i--)
                {
                    if (race.Operations[i].IdCategory == WellOperationCategory.IdBhaUp)
                        dHours += race.Operations[i].DurationHours;
                    if (WellOperationCategory.MechanicalDrillingSubIds.Contains(race.Operations[i].IdCategory))
                        break;
                }
            }
            return dDepth / (dHours + double.Epsilon);
        }

        public async Task<IEnumerable<PlanFactPredictBase<WellOperationDto>>> GetTvdAsync(int idWell, CancellationToken token)
        {
            var wellOperations = await db.WellOperations
                .Include(o => o.OperationCategory)
                .Include(o => o.WellSectionType)
                .Include(o => o.OperationPlan)
                .Where(o => o.IdWell == idWell)
                .OrderBy(o => o.DateStart)
                .ThenBy(o => o.DepthEnd)
                .AsNoTracking()
                .ToListAsync(token)
                .ConfigureAwait(false);

            var wellOperationsPlan = wellOperations
                .Where(o => o.IdType == WellOperation.IdOperationTypePlan)
                .OrderBy(o => o.DateStart)
                .ThenBy(o => o.DepthEnd);

            var wellOperationsFact = wellOperations
                .Where(o => o.IdType == WellOperation.IdOperationTypeFact)
                .OrderBy(o => o.DateStart)
                .ThenBy(o => o.DepthEnd);

            var sectionsIds = wellOperations
                .Select(o => o.IdWellSectionType)
                .Distinct();

            var tzOffsetHours = wellService.GetTimezone(idWell).Hours;

            var merged = MergeArraysBySections(sectionsIds, wellOperationsPlan, wellOperationsFact).ToList();
            var tvd = new List<PlanFactPredictBase<WellOperationDto>>(merged.Count);
            var (Plan, Fact) = merged.FirstOrDefault();
            var dateStart = Plan?.DateStart ?? Fact!.DateStart;
            int? iLastMatch = null;
            int iLastFact = 0;
            var nptHours = 0d;
            for (int i = 0; i < merged.Count; i++)
            {
                var item = merged[i];
                var plan = item.Plan;
                var fact = item.Fact;

                var planFactPredict = new PlanFactPredictBase<WellOperationDto>();
                if (plan is not null)
                {
                    planFactPredict.Plan = Convert(plan, tzOffsetHours);
                    planFactPredict.Plan.Day = (planFactPredict.Plan.DateStart - dateStart).TotalDays;
                    if (fact is not null)
                        iLastMatch = i;
                }

                if (fact is not null)
                {
                    if(WellOperationCategory.NonProductiveTimeSubIds.Contains(fact.IdCategory))
                        nptHours += fact.DurationHours;

                    planFactPredict.Fact = Convert(fact, tzOffsetHours);
                    planFactPredict.Fact.Day = (planFactPredict.Fact.DateStart - dateStart).TotalDays;
                    planFactPredict.Fact.NptHours = nptHours;
                    iLastFact = i;
                }

                tvd.Add(planFactPredict);                    
            }

            if (iLastMatch is null || iLastMatch == merged.Count - 1)
                return tvd;

            var lastMatchPlan = merged[iLastMatch.Value].Plan!;
            var lastMatchPlanOperationEnd = lastMatchPlan.DateStart.AddHours(lastMatchPlan.DurationHours);
            var lastFact = merged[iLastFact].Fact!;
            var lastFactDateEnd = lastFact.DateStart.AddHours(lastFact.DurationHours);
            var startOffset = lastFactDateEnd - lastMatchPlanOperationEnd;

            for (int i = iLastMatch.Value + 1; i < merged.Count; i++)
            {
                if (merged[i].Plan is null)
                    continue;
                var predict = Convert(merged[i].Plan!, tzOffsetHours);
                predict.IdType = 2;
                predict.DateStart = predict.DateStart + startOffset;
                predict.Day = (predict.DateStart - dateStart).TotalDays;
                tvd[i].Predict = predict;
            }

            return tvd;
        }

        private static IEnumerable<(WellOperation? Plan, WellOperation? Fact)> MergeArraysBySections(
            IEnumerable<int> sectionsIds,
            IOrderedEnumerable<WellOperation> wellOperationsPlan,
            IOrderedEnumerable<WellOperation> wellOperationsFact)
        {
            var merged = new List<(WellOperation? Plan, WellOperation? Fact)>(wellOperationsPlan.Count());
            foreach (var sectionId in sectionsIds)
            {
                var sectionOperationsPlan = wellOperationsPlan
                    .Where(o => o.IdWellSectionType == sectionId);
                var sectionOperationsFact = wellOperationsFact
                    .Where(o => o.IdWellSectionType == sectionId);
                var sectionMerged = MergeArrays(sectionOperationsPlan, sectionOperationsFact);
                merged.AddRange(sectionMerged);
            }
            return merged;
        }

        private static IEnumerable<(WellOperation? Plan, WellOperation? Fact)> MergeArrays(IEnumerable<WellOperation> operationsPlan, IEnumerable<WellOperation> operationsFact)
        {
            var operationsFactWithNoPlan = operationsFact.Where(x => x.IdPlan == null).ToArray();
            var operationsFactWithPlan = operationsFact.Where(x => x.IdPlan != null).ToArray();

            var idsPlanWithFact = operationsFact.Where(x => x.IdPlan is not null).Select(x => x.IdPlan).Distinct();
            var operationsPlanWithNoFact = operationsPlan.Where(x => !idsPlanWithFact.Contains(x.IdPlan)).ToArray();

            var result = new List<(WellOperation? Plan, WellOperation? Fact)>(operationsFactWithNoPlan.Length + operationsFactWithPlan.Length + operationsPlanWithNoFact.Length);

            foreach (var operation in operationsFactWithPlan)
                result.Add((operation.OperationPlan, operation));

            foreach (var operation in operationsFactWithNoPlan)
                result.Add((null, operation));

            foreach (var operation in operationsPlanWithNoFact)
                result.Add((operation, null));

            return result
                .OrderBy(x => x.Plan?.DateStart)
                .ThenBy(x => x.Fact?.DateStart);
        }

        private static WellOperationDto Convert(WellOperation source, double tzOffsetHours)
        {
            var destination = source.Adapt<WellOperationDto>();
            destination.CategoryName = source.OperationCategory?.Name;
            destination.WellSectionTypeName = source.WellSectionType?.Caption;
            destination.DateStart = source.DateStart.ToRemoteDateTime(tzOffsetHours);
            return destination;
        }
    }

}