using AsbCloudApp.Data;
using AsbCloudApp.Services;
using AsbCloudDb.Model;
using AsbCloudInfrastructure.Services.Cache;
using Mapster;
using Microsoft.EntityFrameworkCore;
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 IWellService wellService;
        private readonly CacheTable<WellSectionType> cacheSectionsTypes;
        private readonly CacheTable<WellType> cacheWellType;
        private readonly CacheTable<Cluster> cacheCluster;

        public OperationsStatService(IAsbCloudDbContext db, CacheDb cache, IWellService wellService)
        {
            this.db = db;
            this.wellService = wellService;
            cacheSectionsTypes = cache.GetCachedTable<WellSectionType>((DbContext)db);
            cacheWellType = cache.GetCachedTable<WellType>((DbContext)db);
            cacheCluster = cache.GetCachedTable<Cluster>((DbContext)db);
        }

        public async Task<StatClusterDto> GetStatClusterAsync(int idCluster, int idCompany, CancellationToken token = default)
        {
            var allWellsByCompany = await wellService.GetWellsByCompanyAsync(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 cluster = await cacheCluster.FirstOrDefaultAsync(c => c.Id == idCluster, token);
            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> GetWellStatAsync(int idWell,
            CancellationToken token = default)
        {
            var well = await db.Wells
                .Include(w => w.WellOperations)
                .FirstOrDefaultAsync(w => w.Id == idWell, token)
                .ConfigureAwait(false);

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

        public async Task<ClusterRopStatDto> GetRopStatAsync(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 = clusterWellsIds.Select(clusterWellId =>
            {
                var currentWellOps = operations.Where(o => o.IdWell == clusterWellId);
                var timezoneOffsetH = wellService.GetTimezone(clusterWellId).Hours;
                var stat = CalcStat(currentWellOps, timezoneOffsetH);
                return stat;
            }).Where(c => c is not null);

            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 = default)
        {
            var wellType = await cacheWellType.FirstOrDefaultAsync(t => t.Id == well.IdWellType, token);
            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).DateTime,
            };

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

            return statWellDto;
        }

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

            var sectionTypes = cacheSectionsTypes
                .Where(s => sectionTypeIds.Contains(s.Id))
                .ToDictionary(s => s.Id);

            var sections = new List<StatSectionDto>(sectionTypes.Count);
            var operationsPlan = operations.Where(o => o.IdType == WellOperationService.idOperationTypePlan);
            var operationsFact = operations.Where(o => o.IdType == WellOperationService.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 PlanFactBase<StatOperationsDto> GetStatTotal(IEnumerable<WellOperation> operations,
            int idWellState, double timezoneOffsetH)
        {
            var operationsPlan = operations.Where(o => o.IdType == WellOperationService.idOperationTypePlan);
            var operationsFact = operations.Where(o => o.IdType == WellOperationService.idOperationTypeFact);
            var factEnd = CalcStat(operationsFact, timezoneOffsetH);
            if (factEnd is not null && idWellState != 2)
                factEnd.End = null;
            var section = new PlanFactBase<StatOperationsDto>
            {
                Plan = CalcStat(operationsPlan, timezoneOffsetH),
                Fact = factEnd,
            };
            return section;
        }

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

            return CalcStat(sectionOperations, timezoneOffsetH);
        }

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

            var races = GetCompleteRaces(operations, timezoneOffsetH);

            var section = new StatOperationsDto
            {
                Start = operations.FirstOrDefault()?.DateStart.ToRemoteDateTime(timezoneOffsetH),
                End = operations.Max(o => o.DateStart.ToRemoteDateTime(timezoneOffsetH).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 => o.IdCategory == WellOperationService.idOperationNonProductiveTime)
                    .Sum(o => o.DurationHours),
            };
            return section;
        }

        private static double CalcROP(IEnumerable<WellOperation> operationsProps)
        {
            var drillingOperations = operationsProps.Where(o => o.IdCategory == WellOperationService.idOperationDrilling);
            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 == WellOperationService.idOperationCasingDown);
            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 == WellOperationService.idOperationBhaAssembly)
                {
                    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 == WellOperationService.idOperationNonProductiveTime)
                        {
                            race.NonProductiveHours += iterator.Current.DurationHours;
                        }
                        if (iterator.Current.IdCategory == WellOperationService.idOperationBhaDisassembly)
                        {
                            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.DeltaHoursTimeNoNpt;
                dDepth += race.DeltaDepth;
            }
            return dDepth / (dHours + double.Epsilon);
        }

        private static double CalcBhaDownSpeed(IEnumerable<Race> races)
        {
            var dDepth = 0d;
            var dHours = 0d;
            foreach (var race in races)
            {
                dDepth += race.StartWellDepth;
                for (var i = 0; i < race.Operations.Count; i++)
                {
                    if (race.Operations[i].IdCategory == WellOperationService.idOperationBhaDown)
                        dHours += race.Operations[i].DurationHours;
                    if (race.Operations[i].IdCategory == WellOperationService.idOperationDrilling)
                        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 == WellOperationService.idOperationBhaUp)
                        dHours += race.Operations[i].DurationHours;
                    if (race.Operations[i].IdCategory == WellOperationService.idOperationDrilling)
                        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)
                .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 == WellOperationService.idOperationTypePlan)
                .OrderBy(o => o.DateStart)
                .ThenBy(o => o.DepthEnd);

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

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

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

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

            var merged = MergeArraysBySections(sectionsIds, wellOperationsPlan, wellOperationsFact);
            var tvd = new List<PlanFactPredictBase<WellOperationDto>>(merged.Count);
            var firstPoint = merged.First();
            var dateStart = firstPoint.Item1?.DateStart ?? firstPoint.Item2.DateStart;
            int iLastMatch = 0;
            int iLastFact = 0;
            var nptHours = 0d;
            for (int i = 0; i < merged.Count; i++)
            {
                var item = merged[i];
                var plan = item.Item1;
                var fact = item.Item2;

                if (fact?.IdCategory == WellOperationService.idOperationNonProductiveTime)
                {
                    nptHours += fact.DurationHours;
                }

                var planFactPredict = new PlanFactPredictBase<WellOperationDto>
                {
                    Plan = Convert(plan, tzOffsetHours),
                    Fact = Convert(fact, tzOffsetHours),
                    Predict = null,
                };

                if (planFactPredict.Plan is not null)
                    planFactPredict.Plan.Day = (planFactPredict.Plan.DateStart - dateStart).TotalDays;

                if (planFactPredict.Fact is not null)
                {
                    planFactPredict.Fact.Day = (planFactPredict.Fact.DateStart - dateStart).TotalDays;
                    planFactPredict.Fact.NptHours = nptHours;
                }

                tvd.Add(planFactPredict);
                if ((plan is not null) && (fact is not null))
                    iLastMatch = i;
                if (fact is not null)
                    iLastFact = i;
            }

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

            var lastMatchPlan = merged[iLastMatch].Item1;
            var lastMatchPlanOperationEnd = lastMatchPlan.DateStart.AddHours(lastMatchPlan.DurationHours);
            //var lastMatchFact = merged[iLastMatch].Item2;
            //var lastMatchFactDateEnd = lastMatchFact.DateStart.AddHours(lastMatchFact.DurationHours);
            var lastFact = merged[iLastFact].Item2;
            var lastFactDateEnd = lastFact.DateStart.AddHours(lastFact.DurationHours);
            var startOffset = lastFactDateEnd - lastMatchPlanOperationEnd;

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

            return tvd;
        }

        private static List<Tuple<WellOperation, WellOperation>> MergeArraysBySections(
            IEnumerable<int> sectionsIds,
            IOrderedEnumerable<WellOperation> wellOperationsPlan,
            IOrderedEnumerable<WellOperation> wellOperationsFact)
        {
            var merged = new List<Tuple<WellOperation, WellOperation>>(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 List<Tuple<WellOperation, WellOperation>> MergeArrays(IEnumerable<WellOperation> array1, IEnumerable<WellOperation> array2)
        {
            var a1 = array1.ToArray();
            var a2 = array2.ToArray();

            var m = new List<Tuple<WellOperation, WellOperation>>(a1.Length);
            void Add(WellOperation item1, WellOperation item2) =>
                 m.Add(new Tuple<WellOperation, WellOperation>(item1, item2));

            static bool Compare(WellOperation item1, WellOperation item2) =>
                item1.IdCategory == item2.IdCategory && Math.Abs(item1.DepthEnd - item2.DepthEnd) < (30d + 0.005d * (item1.DepthEnd + item2.DepthEnd));

            int i1 = 0;
            int i2 = 0;
            while (true)
            {
                var is1 = a1.Length > i1;
                var is2 = a2.Length > i2;
                if (!(is1 || is2))
                    break;

                if (is1 && is2)
                {
                    if (Compare(a1[i1], a2[i2]))
                        Add(a1[i1++], a2[i2++]);
                    else
                    {
                        int nextI1 = Array.FindIndex(a1, i1, (item) => Compare(item, a2[i2]));
                        int nextI2 = Array.FindIndex(a2, i2, (item) => Compare(item, a1[i1]));

                        bool deltaI1_Lt_deltaI2 = (nextI1 - i1) < (nextI2 - i2);

                        if (nextI1 == -1 && nextI2 == -1)
                        {
                            if (a1[i1].DepthEnd < a2[i2].DepthEnd)
                            {
                                Add(a1[i1++], null);
                            }
                            else
                            {
                                Add(null, a2[i2++]);
                            }
                        }
                        else if (nextI1 > -1 && nextI2 == -1)
                        {
                            Add(a1[i1++], null);
                        }
                        else if (nextI1 == -1 && nextI2 > -1)
                        {
                            Add(null, a2[i2++]);
                        }
                        else if (deltaI1_Lt_deltaI2)
                        {
                            Add(a1[i1++], null);
                        }
                        else if (!deltaI1_Lt_deltaI2)
                        {
                            Add(null, a2[i2++]);
                        }
                    }
                }
                else if (is1)
                {
                    Add(a1[i1++], null);
                }
                else if (is2)
                {
                    Add(null, a2[i2++]);
                }
            }

            return m;
        }

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