using AsbCloudApp.Data.ProcessMap;
using AsbCloudApp.Data.SAUB;
using AsbCloudApp.Exceptions;
using AsbCloudApp.Repositories;
using AsbCloudApp.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace AsbCloudInfrastructure.Services.ProcessMap
{

    public partial class ProcessMapReportService : IProcessMapReportService
    {
        private readonly IWellService wellService;
        private readonly IWellOperationRepository wellOperationRepository;
        private readonly IProcessMapPlanRepository processMapPlanRepository;
        private readonly ITelemetryDataSaubService telemetryDataSaubService;

        public ProcessMapReportService(
            IWellService wellService,
            IWellOperationRepository wellOperationRepository,
            IProcessMapPlanRepository processMapPlanRepository,
            ITelemetryDataSaubService telemetryDataSaubService)
        {
            this.wellService = wellService;
            this.wellOperationRepository = wellOperationRepository;
            this.processMapPlanRepository = processMapPlanRepository;
            this.telemetryDataSaubService = telemetryDataSaubService;
        }

        /// <inheritdoc/>
        public async Task<IEnumerable<ProcessMapReportDto>> GetProcessMapReportAsync(int idWell, CancellationToken token)
        {
            var well = wellService.GetOrDefault(idWell)
                ?? throw new ArgumentInvalidException("idWell not found", nameof(idWell));

            var idTelemetry = well.IdTelemetry
                ?? throw new ArgumentInvalidException("telemetry by well not found", nameof(idWell));

            var processMapPlan = await processMapPlanRepository.GetByIdWellAsync(idWell, token);

            if (!processMapPlan.Any())
                return Enumerable.Empty<ProcessMapReportDto>();

            var telemetryDataStat = (await telemetryDataSaubService.GetTelemetryDataStatAsync(idTelemetry, token)).ToArray();
            if (!telemetryDataStat.Any())
                return Enumerable.Empty<ProcessMapReportDto>();

            var result = CalcByIntervals(processMapPlan, telemetryDataStat);

            return result;
        }

        private IEnumerable<ProcessMapReportDto> CalcByIntervals(IEnumerable<ProcessMapPlanDto> processMapPlan, TelemetryDataSaubStatDto[] telemetryDataStat)
        {
            var processMapIntervals = CalcDepthIntervals(processMapPlan);

            var result = new List<ProcessMapReportDto>(processMapIntervals.Count() * 4);

            var telemetryIndexStart = Array.FindIndex(telemetryDataStat, t => t.WellDepthMin >= processMapIntervals.First().DepthStart);
            if (telemetryIndexStart < 0)
                return Enumerable.Empty<ProcessMapReportDto>();

            IDictionary<int, string> sectionTypes = wellOperationRepository.GetSectionTypes();

            foreach (var interval in processMapIntervals)
            {
                // plans    [  ][  ]
                // interval   [  ]
                var processMapPlanInterval = processMapPlan
                    .Where(p => p.DepthStart <= interval.DepthEnd && p.DepthEnd >= interval.DepthStart);

                if (!processMapPlanInterval.Any())
                    continue;

                var telemetryIndexEnd = Array.FindIndex(telemetryDataStat, telemetryIndexStart, t => t.WellDepthMin >= interval.DepthEnd);
                if (telemetryIndexEnd < 0)
                    telemetryIndexEnd = telemetryDataStat.Length - 1;
                var telemetryDataInterval = telemetryDataStat.AsSpan(telemetryIndexStart, telemetryIndexEnd - telemetryIndexStart);

                IEnumerable<ProcessMapReportDto> subIntervalsResult = CalcSubIntervals(interval, processMapPlanInterval, telemetryDataInterval, sectionTypes);

                result.AddRange(subIntervalsResult);
                telemetryIndexStart = telemetryIndexEnd;
            }

            return result;
        }

        private static IEnumerable<(double DepthStart, double DepthEnd)> CalcDepthIntervals(IEnumerable<ProcessMapPlanDto> processMapPlan)
        {
            if(!processMapPlan.Any())
                yield break;

            var intervalStarts = processMapPlan
                .OrderBy(i => i.DepthStart)
                .Select(p => p.DepthStart)
                .Distinct()
                .ToArray();

            for (var i = 1; i < intervalStarts.Length; i++)
                yield return (intervalStarts[i - 1], intervalStarts[i]);

            yield return (intervalStarts[^1], processMapPlan.Max(p=>p.DepthEnd));
        }

        private static IEnumerable<ProcessMapReportDto> CalcSubIntervals(
            (double DepthStart, double DepthEnd) interval,
            IEnumerable<ProcessMapPlanDto> processMapPlanInterval,
            Span<TelemetryDataSaubStatDto> telemetryDataInterval,
            IDictionary<int, string> sectionTypes)
        {
            var telemetryDataIntervalLength = telemetryDataInterval.Length;
            if (telemetryDataInterval.Length == 0)
                return Enumerable.Empty<ProcessMapReportDto>();

            var result = new List<ProcessMapReportDto>();
            var telemetryIndexStart = 0;
            var subInterval = interval;

            for (var i = telemetryIndexStart + 1; i < telemetryDataIntervalLength; i++)
            {
                if (IsDifferent(telemetryDataInterval[telemetryIndexStart], telemetryDataInterval[i]))
                {
                    subInterval.DepthEnd = telemetryDataInterval[i - 1].WellDepthMax;
                    var telemetryRowSpan = telemetryDataInterval[telemetryIndexStart..(i - 1)];
                    if (!telemetryRowSpan.IsEmpty)
                    {
                        var intervalReportRow = CalcSubIntervalReportRow(subInterval, processMapPlanInterval, telemetryRowSpan, sectionTypes);
                        result.Add(intervalReportRow);
                    }                        
                    telemetryIndexStart = i;
                    subInterval.DepthStart = subInterval.DepthEnd;
                }
            }

            subInterval.DepthEnd = interval.DepthEnd;
            var intervalReportRowLast = CalcSubIntervalReportRow(subInterval, processMapPlanInterval, telemetryDataInterval[telemetryIndexStart..telemetryDataIntervalLength], sectionTypes);
            result.Add(intervalReportRowLast);
            return result;
        }

        private static ProcessMapReportDto CalcSubIntervalReportRow(
            (double DepthStart, double DepthEnd) subInterval, 
            IEnumerable<ProcessMapPlanDto> processMap, 
            Span<TelemetryDataSaubStatDto> telemetryRowSpan, 
            IDictionary<int, string> sectionTypes)
        {
            var telemetryStat = new TelemetryStat(telemetryRowSpan);
            var processMapByMode = processMap.FirstOrDefault(p => p.IdMode == telemetryStat.IdMode);
            var processMapFirst = processMap.First();
            var idWellSectionType = processMapByMode?.IdWellSectionType ?? processMapFirst.IdWellSectionType;
            
            var result = new ProcessMapReportDto
            {
                IdWell = processMapByMode?.IdWell ?? processMapFirst.IdWell,
                IdWellSectionType = idWellSectionType,
                WellSectionTypeName = sectionTypes[idWellSectionType],

                DepthStart = subInterval.DepthStart,
                DepthEnd = subInterval.DepthEnd,
                DateStart = telemetryStat.DateStart,

                MechDrillingHours = telemetryStat.MechDrillingHours,
                DrillingMode = telemetryStat.ModeName,

                DeltaDepth = telemetryStat.DeltaDepth,

                PressureDiff = telemetryStat.Pressure.MakeParams(processMapByMode?.Pressure.Plan),
                AxialLoad = telemetryStat.AxialLoad.MakeParams(processMapByMode?.AxialLoad.Plan),
                TopDriveTorque = telemetryStat.RotorTorque.MakeParams(processMapByMode?.TopDriveTorque.Plan),
                SpeedLimit = telemetryStat.BlockSpeed.MakeParams(processMapByMode?.RopPlan),

                Rop = telemetryStat.Rop,
                UsagePlan = processMapByMode?.UsageSaub ?? telemetryStat.UsagePredictPlan,
                UsageFact = telemetryStat.UsageSaub,
            };
            return result;
        }

        private static bool IsDifferent(TelemetryDataSaubStatDto intervalStart, TelemetryDataSaubStatDto current)
        {
            if (intervalStart.WellDepthMin > current.WellDepthMin)
                return true;

            if (intervalStart.IdMode != current.IdMode)
                return true;

            if (Math.Abs(intervalStart.PressureSp - current.PressureSp) > 5d)
                return true;

            if (Math.Abs(intervalStart.AxialLoadSp - current.AxialLoadSp) > 1d)
                return true;

            if (Math.Abs(intervalStart.RotorTorqueSp - current.RotorTorqueSp) > 5d)
                return true;

            var blockSpeedSpDiff = Math.Abs(intervalStart.BlockSpeedSp - current.BlockSpeedSp);
            if (blockSpeedSpDiff > 5d)
            {
                if (intervalStart.BlockSpeedSp <= 30)
                    return true;
                else if (intervalStart.BlockSpeedSp > 30 && blockSpeedSpDiff > 15d)
                    return true;
                else if (intervalStart.BlockSpeedSp > 80 && blockSpeedSpDiff > 20d)
                    return true;
            }

            return false;
        }
    }

    class ParamStat 
    {
        private double spWSum;
        private double pvWSum;
        private double limitMaxWSum;

        private double deltaDepthSum;

        private readonly Func<TelemetryDataSaubStatDto, double> getterSp;
        private readonly Func<TelemetryDataSaubStatDto, double> getterPv;
        private readonly Func<TelemetryDataSaubStatDto, double>? getterLimitMax;

        private readonly int idFeedRegulator;
        private readonly int idMode;
        private TelemetryDataSaubStatDto? previous;

        public double SpUsageDepth { get; private set; }
        private static double spUsageTotal;

        public ParamStat(Func<TelemetryDataSaubStatDto, double> getterSp,
                         Func<TelemetryDataSaubStatDto, double> getterPv,
                         Func<TelemetryDataSaubStatDto, double>? getterLimitMax,
                         int idFeedRegulator,
                         int idMode)
        {
            this.getterSp = getterSp;
            this.getterPv = getterPv;
            this.getterLimitMax = getterLimitMax;
            this.idFeedRegulator = idFeedRegulator;
            this.idMode = idMode;
            spUsageTotal = 0d;
        }

        public void UpdateStat(TelemetryDataSaubStatDto current)
        {
            if(previous is not null)
            {
                var deltaDepth = current.WellDepthMin - previous.WellDepthMin;
                if (deltaDepth > 0)
                {
                    var deltaDepthHalf = deltaDepth / 2;
                    double CalculateWeight(Func<TelemetryDataSaubStatDto, double> getter) => (getter(previous!) + getter(current)) * deltaDepthHalf;
                    
                    spWSum += CalculateWeight(getterSp);
                    pvWSum += CalculateWeight(getterPv);
                    if(getterLimitMax is not null)
                        limitMaxWSum += CalculateWeight(getterLimitMax!);

                    if (current.IdFeedRegulator is not null)
                    {
                        if (current.IdFeedRegulator == idFeedRegulator)
                        {
                            SpUsageDepth += deltaDepth;
                            spUsageTotal += deltaDepth;
                        }
                    }
                    else
                    {
                        var pvErr = (getterSp(current) - getterPv(current)) / getterSp(current);
                        if (pvErr < 0.03d) //3%
                        {
                            SpUsageDepth += deltaDepth;
                            spUsageTotal += deltaDepth;
                        }
                    }                    

                    deltaDepthSum += deltaDepth;
                }
            }

            previous = current;
        }

        public ProcessMapReportParamsDto MakeParams(double? spPlan)
        {
            var result = new ProcessMapReportParamsDto
            {
                SetpointPlan = spPlan,
                Fact = DivideValByDepth(pvWSum),
            };

            if (idMode == 0)
            {
                result.SetpointFact = null;
                result.Limit = null;
                result.SetpointUsage = null;
            }
            else
            {
                result.SetpointFact = DivideValByDepth(spWSum);
                result.Limit = (getterLimitMax is not null) ? DivideValByDepth(limitMaxWSum) : null;
                result.SetpointUsage = deltaDepthSum > 0d ? 100d * SpUsageDepth / spUsageTotal : null;
            }
                
            return result;
        }

        private double? DivideValByDepth(double? val)
        {
            if(val is null || val == 0d || deltaDepthSum == 0d)
                return null;
            return val / deltaDepthSum;
        }
    }

    class TelemetryStat {
        public ParamStat Pressure { get; }
        public ParamStat AxialLoad {get; }
        public ParamStat RotorTorque {get; }
        public ParamStat BlockSpeed  {get; }

        private TelemetryDataSaubStatDto? previous;
        private double depthSum = 0d;
        private double hoursSum = 0d;

        public double? Rop => hoursSum == 0d ? null : depthSum / hoursSum;

        private double depthWithSaub = 0d;
        public double UsageSaub { get; }
        public double UsagePredictPlan { get; }
        public DateTime DateStart { get;  }
        public float DeltaDepth { get; }
        public int IdMode { get;  }
        public string ModeName { get;  }
        public double MechDrillingHours { get; }

        public TelemetryStat(Span<TelemetryDataSaubStatDto> telemetry)
        {
            var telemetryFirst = telemetry[0];
            var telemetryLast = telemetry[^1];

            IdMode = telemetryFirst.IdMode;
            ModeName = GetModeName(IdMode);
            DateStart = telemetryFirst.DateMin;
            DeltaDepth = telemetryLast.WellDepthMax - telemetryFirst.WellDepthMin;
            MechDrillingHours = (telemetryLast.DateMax - telemetryFirst.DateMin).TotalHours;

            BlockSpeed = new(t => t.BlockSpeedSp, t => t.BlockSpeed, null, 1, IdMode);
            Pressure = new(t => t.PressureSpDelta, t => t.PressureDelta, t=>t.PressureDeltaLimitMax, 2, IdMode);
            RotorTorque = new(t => t.RotorTorqueSp, t => t.RotorTorque, t=>t.RotorTorqueLimitMax, 3, IdMode);
            AxialLoad = new(t => t.AxialLoadSp, t => t.AxialLoad, t=>t.AxialLoadLimitMax, 4, IdMode);

            for (int i = 0; i < telemetry.Length; i++)
                UpdateStat(telemetry[i]);

            UsageSaub = 100d * depthWithSaub / depthSum;
            UsagePredictPlan = IdMode != 0 ? 100d : 0d;
        }

        private void UpdateStat(TelemetryDataSaubStatDto current)
        {
            if(previous is not null)
            {
                var deltaDepth = current.WellDepthMin - previous.WellDepthMin;
                if(deltaDepth > 0)
                {
                    var deltaHours = (current.DateMin - previous.DateMax).TotalHours;
                    depthSum += deltaDepth;
                    hoursSum += deltaHours;

                    if(current.IdMode == 1 || current.IdMode == 3)
                        depthWithSaub += deltaDepth;
                }
            }

            previous = current;
            Pressure.UpdateStat(current);
            AxialLoad.UpdateStat(current);
            RotorTorque.UpdateStat(current);
            BlockSpeed.UpdateStat(current);
        }

        private static string GetModeName(int idMode)
            => idMode switch
            {
                1 => "Ротор",
                3 => "Слайд",
                _ => "Ручной",
            };
    }

}