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
{
    class OperationParams
    {
        public OperationParams() { }

        public OperationParams(WellOperation operation)
        {
            Id = operation.Id;
            IdWellSectionType = operation.IdWellSectionType;
            IdCategory = operation.IdCategory;

            Start = operation.StartDate;

            WellDepth = operation.WellDepth;
            DeltaDepth = 0;
            Hours = operation.DurationHours;
        }
        public int IdWellSectionType { get; }
        public int IdCategory { get; }
        public int Id { get; }
        public DateTime Start { get; }
        public double WellDepth { get; set; }
        public double DeltaDepth { get; set; }
        public double Hours { get; set; }
    }

    class Race
    {
        public DateTime StartDate { get; set; }
        public double StartWellDepth { get; set; }
        public DateTime EndDate { get; set; }
        public double EndWellDepth { get; set; }
        public double DrillingTime { get; set; }
        public double NonProductiveHours { get; set; }
        public double DeltaDepth => EndWellDepth - StartWellDepth;
        public double DeltaHoursTimeNoNpt => (EndDate - StartDate).TotalHours - NonProductiveHours;
        public double Speed => DeltaDepth / (DeltaHoursTimeNoNpt + double.Epsilon);

        public List<OperationParams> Operations { get; internal set; }
    }

    public class WellOperationsStatService : IWellOperationsStatService
    {
        private readonly IAsbCloudDbContext db;
        private readonly IWellService wellService;
        private readonly CacheTable<WellSectionType> cacheSectionsTypes;
        private readonly CacheTable<Well> cacheWell;
        private readonly CacheTable<WellType> cacheWellType;
        private readonly CacheTable<Cluster> cacheCluster;
        private const int idOperationBhaAssembly = 1025;
        private const int idOperationBhaDisassembly = 1026;
        private const int idOperationNonProductiveTime = 1043;
        private const int idOperationDrilling = 1001;
        private const int idOperationBhaDown = 1046;
        private const int idOperationBhaUp = 1047;
        private const int idOperationCasingDown = 1048;
        private const int idOperationTypePlan = 0;
        private const int idOperationTypeFact = 1;

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

        public async Task<StatClusterDto> GetStatClusterAsync(int idCluster, CancellationToken token = default)
        {
            var wells = await db.Wells
                .Include(w => w.WellOperations)
                .Where(o => o.IdCluster == idCluster)
                .AsNoTracking()
                .ToListAsync(token);

            var operations = wells
                .SelectMany(w => w.WellOperations)
                .OrderBy(o => o.StartDate)
                .ThenBy(o => o.WellDepth);

            var cluster = await cacheCluster.FirstOrDefaultAsync(c => c.Id == idCluster, token);

            var wellsIds = wells.Select(o => o.Id).Distinct();

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

            foreach (var idWell in wellsIds)
            {
                var statWellDto = await CalcStatWell(operations, idWell, token);
                statsWells.Add(statWellDto);
            }

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

        public async Task<StatWellDto> GetStatWellAsync(int idWell,
            CancellationToken token = default)
        {
            var operations = await db.WellOperations
                .Where(o => o.IdWell == idWell)
                .OrderBy(o => o.StartDate) // ускорит дальнейшие сортировки
                .ThenBy(o => o.WellDepth)
                .AsNoTracking()
                .ToListAsync(token);

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

            var statWellDto = await CalcStatWell(operations, idWell, token);
            return statWellDto;
        }

        private async Task<StatWellDto> CalcStatWell(IEnumerable<WellOperation> operations, int idWell,
            CancellationToken token = default)
        {
            var wellOperations = operations
                .Where(o => o.IdWell == idWell);

            var well = await cacheWell.FirstOrDefaultAsync(w => w.Id == idWell, token);
            var wellType = await cacheWellType.FirstOrDefaultAsync(t => t.Id == well.IdWellType, token);

            if (!wellOperations.Any())
                return new StatWellDto()
                {
                    Id = idWell,
                    Caption = well.Caption,
                    WellType = wellType.Caption
                };

            var statWellDto = new StatWellDto
            {
                Id = idWell,
                Caption = well.Caption,
                WellType = wellType?.Caption,
                Companies = await wellService.GetCompaniesAsync(idWell, token),
                Sections = CalcSectionsStats(wellOperations),
                Total = GetStat(wellOperations),
            };
            return statWellDto;
        }

        private IEnumerable<StatSectionDto> CalcSectionsStats(IEnumerable<WellOperation> operations)
        {
            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 = MakeOperationsExt(operations.Where(o => o.IdType == idOperationTypePlan));
            var operationsFact = MakeOperationsExt(operations.Where(o => o.IdType == idOperationTypeFact));

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

        private static PlanFactBase<StatOperationsDto> GetStat(IEnumerable<WellOperation> operations)
        {
            var operationsPlan = MakeOperationsExt(operations.Where(o => o.IdType == idOperationTypePlan));
            var operationsFact = MakeOperationsExt(operations.Where(o => o.IdType == idOperationTypeFact));
            var section = new PlanFactBase<StatOperationsDto>
            {
                Plan = CalcStat(operationsPlan),
                Fact = CalcStat(operationsFact),
            };
            return section;
        }

        private static StatOperationsDto CalcSectionStat(IEnumerable<OperationParams> operations, int idSectionType)
        {
            var sectionOperations = operations
                .Where(o => o.IdWellSectionType == idSectionType)
                .OrderBy(o => o.Start)
                .ThenBy(o => o.WellDepth);

            return CalcStat(sectionOperations);
        }

        private static StatOperationsDto CalcStat(IEnumerable<OperationParams> operations)
        {
            if (!operations.Any())
                return null;

            var races = GetCompleteRaces(operations);

            var section = new StatOperationsDto
            {
                Start = operations.First().Start,
                End = operations.Max(o => (o.Start.AddHours(o.Hours))),
                WellDepthStart = operations.Min(o => o.WellDepth),
                WellDepthEnd = operations.Max(o => o.WellDepth),
                Rop = CalcROP(operations),
                RouteSpeed = CalcAvgRaceSpeed(races),
                BhaDownSpeed = CalcBhaDownSpeed(races),
                BhaUpSpeed = CalcBhaUpSpeed(races),
                CasingDownSpeed = CalcCasingDownSpeed(operations),
                NonProductiveHours = operations
                    .Where(o => o.IdCategory == idOperationNonProductiveTime)
                    .Sum(o => o.Hours),
            };
            return section;
        }

        private static double CalcROP(IEnumerable<OperationParams> operationsProps)
        {
            var drillingOperations = operationsProps.Where(o => o.IdCategory == idOperationDrilling);
            var dDepth = 0d;
            var dHours = 0d;
            foreach (var operation in drillingOperations)
            {
                dDepth += operation.DeltaDepth;
                dHours += operation.Hours;
            }
            return dDepth / (dHours + double.Epsilon);
        }

        private static double CalcCasingDownSpeed(IEnumerable<OperationParams> operationsProps)
        {
            var ops = operationsProps.Where(o => o.IdCategory == idOperationCasingDown);
            var depth = 0d;
            var dHours = 0d;
            foreach (var operation in ops)
            {
                depth += operation.WellDepth;
                dHours += operation.Hours;
            }
            return depth / (dHours + double.Epsilon);
        }

        private static IEnumerable<Race> GetCompleteRaces(IEnumerable<OperationParams> operations)
        {
            var races = new List<Race>();
            var iterator = operations
                .OrderBy(o => o.Start)
                .GetEnumerator();
            while (iterator.MoveNext())
            {
                if (iterator.Current.IdCategory == idOperationBhaAssembly)
                {
                    var race = new Race
                    {
                        StartDate = iterator.Current.Start.AddHours(iterator.Current.Hours),
                        StartWellDepth = iterator.Current.WellDepth,
                        Operations = new List<OperationParams>(4),
                    };
                    while (iterator.MoveNext())
                    {
                        if (iterator.Current.IdCategory == idOperationNonProductiveTime)
                        {
                            race.NonProductiveHours += iterator.Current.Hours;
                        }
                        if (iterator.Current.IdCategory == idOperationBhaDisassembly)
                        {
                            race.EndDate = iterator.Current.Start;
                            race.EndWellDepth = iterator.Current.WellDepth;
                            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 == idOperationBhaDown)
                        dHours += race.Operations[i].Hours;
                    if (race.Operations[i].IdCategory == 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 == idOperationBhaUp)
                        dHours += race.Operations[i].Hours;
                    if (race.Operations[i].IdCategory == 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.StartDate)
                .ThenBy(o => o.WellDepth)
                .AsNoTracking()
                .ToListAsync(token)
                .ConfigureAwait(false);

            var wellOperationsPlan = wellOperations
                .Where(o => o.IdType == idOperationTypePlan)
                .OrderBy(o => o.StartDate)
                .ThenBy(o => o.WellDepth);

            var wellOperationsFact = wellOperations
                .Where(o => o.IdType == idOperationTypeFact)
                .OrderBy(o => o.StartDate)
                .ThenBy(o => o.WellDepth);

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

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

            var merged = MergeArraysBySections(sectionsIds, wellOperationsPlan, wellOperationsFact);
            var tvd = new List<PlanFactPredictBase<WellOperationDto>>(merged.Count);
            int iLastMatch = 0;
            int iLastFact = 0;
            for (int i = 0; i < merged.Count; i++)
            {
                var item = merged[i];

                var planFactPredict = new PlanFactPredictBase<WellOperationDto>
                {
                    Plan = item.Item1?.Adapt<WellOperationDto, WellOperation>(WellOperationDtoMutation),
                    Fact = item.Item2?.Adapt<WellOperationDto, WellOperation>(WellOperationDtoMutation),
                    Predict = null,
                };

                tvd.Add(planFactPredict);
                if ((item.Item1 is not null) && (item.Item2 is not null))
                    iLastMatch = i;
                if (item.Item2 is not null)
                    iLastFact = i;
            }

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

            var lastMatchPlan = merged[iLastMatch].Item1;
            var lastMatchPlanOperationEnd = lastMatchPlan.StartDate.AddHours(lastMatchPlan.DurationHours);
            var lastMatchFact = merged[iLastMatch].Item2;
            var startOffset = lastMatchFact.StartDate.AddHours(lastMatchFact.DurationHours) - lastMatchPlanOperationEnd;

            for (int i = iLastMatch + 1; i < merged.Count; i++)
            {
                if (merged[i].Item1 is null)
                    continue;
                tvd[i].Predict = merged[i].Item1.Adapt<WellOperationDto>();
                tvd[i].Predict.IdType = 2;
                tvd[i].Predict.StartDate = tvd[i].Predict.StartDate + startOffset;
            }
            return tvd;
        }

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

        public 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.WellDepth - item2.WellDepth) < (30d + 0.005d*(item1.WellDepth + item2.WellDepth));

            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<WellOperation>(a1, i1, (item) => Compare(item, a2[i2]));
                        int nextI2 = Array.FindIndex<WellOperation>(a2, i2, (item) => Compare(item, a1[i1]));

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

                        if (nextI1 == -1 && nextI2 == -1)
                        {
                            if (a1[i1].WellDepth < a2[i2].WellDepth)
                            {
                                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 readonly Action<WellOperationDto, WellOperation> WellOperationDtoMutation = (WellOperationDto dest, WellOperation source) =>
        {
            dest.CategoryName = source.OperationCategory?.Name;
            dest.WellSectionTypeName = source.WellSectionType?.Caption;
        };

        private static IEnumerable<OperationParams> MakeOperationsExt(IEnumerable<WellOperation> operations)
        {
            var ops = new List<OperationParams>();

            if (operations.Any())
            {
                var sortedOperations = operations
                    .OrderBy(o => o.StartDate)
                    .ThenBy(o => o.WellDepth);
                var count = operations.Count();
                ops = new List<OperationParams>(count);
                var item = operations.ElementAt(0);
                var wellDepth = item.WellDepth;
                var pre = new OperationParams(item);
                var current = new OperationParams(item);
                for (int i = 1; i < count; i++)
                {
                    item = operations.ElementAt(i);
                    current = new OperationParams(item) { WellDepth = Helper.Max(wellDepth, item.WellDepth) };
                    pre.DeltaDepth = current.WellDepth - wellDepth;
                    wellDepth = current.WellDepth;
                    pre.Hours = (current.Start - pre.Start).TotalHours;
                    ops.Add(pre);
                    pre = current;
                }
                ops.Add(current);
            }

            return ops;
        }
    }
}