using AsbCloudApp.Data;
using AsbCloudApp.Data.ProcessMap;
using AsbCloudApp.Data.SAUB;
using AsbCloudApp.Exceptions;
using AsbCloudApp.Repositories;
using AsbCloudApp.Requests;
using AsbCloudApp.Services;
using AsbCloudApp.Services.Subsystems;
using AsbCloudDb.Model;
using AsbCloudApp.Data.Subsystems;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AsbCloudInfrastructure.Services.Subsystems;

namespace AsbCloudInfrastructure.Services.ProcessMap
{
#nullable enable
    public partial class ProcessMapService : IProcessMapService
    {
        private readonly IWellService wellService;
        private readonly IWellOperationRepository wellOperationRepository;
        private readonly IProcessMapRepository processMapRepository;
        private readonly ITelemetryDataSaubService telemetryDataSaubService;
        private readonly ILimitingParameterRepository limitingParameterRepository;
        private readonly ISubsystemOperationTimeService subsystemOperationTimeService;

        public ProcessMapService(
            IWellService wellService,
            IWellOperationRepository wellOperationService,
            IProcessMapRepository processMapRepository,
            ITelemetryDataSaubService telemetryDataSaubService,
            ILimitingParameterRepository limitingParameterRepository,
            ISubsystemOperationTimeService subsystemOperationTimeService)
        {
            this.wellService = wellService;
            this.wellOperationRepository = wellOperationService;
            this.processMapRepository = processMapRepository;
            this.telemetryDataSaubService = telemetryDataSaubService;
            this.limitingParameterRepository = limitingParameterRepository;
            this.subsystemOperationTimeService = subsystemOperationTimeService;
        }

        /// <inheritdoc/>
        public async Task<IEnumerable<ProcessMapReportDto>> GetProcessMapAsync(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 processMap = (await processMapRepository.GetByIdWellAsync(idWell, token))!;
            var factDrillingOperations = await GetFactDrillingOperationsAsync(idWell, token);
            var telemetryDataStat = await telemetryDataSaubService.GetTelemetryDataStatAsync(idTelemetry, token);
            var limitingParameters = await limitingParameterRepository.GetLimitingParametersAsync(new(), well, token);
            var subsystemsOperationTime = await GetOperationTimeAsync(idWell, token);

            var result = factDrillingOperations
                .GroupBy(o => o.IdWellSectionType)
                .SelectMany(sectionOperations =>
                {
                    var sectionProcessMap = processMap.Where(p => p.IdWellSectionType == sectionOperations.Key);
                    return HandleSection(sectionOperations, sectionProcessMap, telemetryDataStat, limitingParameters, subsystemsOperationTime!);
                })
                .ToList();

            return result;
        }

        private Task<IEnumerable<SubsystemOperationTimeDto>?> GetOperationTimeAsync(int idWell, CancellationToken token)
        {
            var request = new SubsystemOperationTimeRequest
            {
                IdWell = idWell,
                IdsSubsystems = new int[] { SubsystemOperationTimeService.IdSubsystemAKB, SubsystemOperationTimeService.IdSubsystemSpin },
            };
            return subsystemOperationTimeService.GetOperationTimeAsync(request, token);
        }

        private async Task<IEnumerable<WellOperationDto>> GetFactDrillingOperationsAsync(int idWell, CancellationToken token)
        {
            var operationsRequest = new WellOperationRequest
            {
                IdWell = idWell,
                OperationCategoryIds = WellOperationCategory.MechanicalDrillingSubIds,
                OperationType = WellOperation.IdOperationTypeFact,
                SortFields = new[] { nameof(WellOperation.DateStart) }
            };

            var allFactDrillingOperations = await wellOperationRepository.GetAsync(operationsRequest, token);
            var factDrillingOperations = allFactDrillingOperations.Where(o => o.DepthEnd > o.DepthStart);
            return factDrillingOperations;
        }

        private static IEnumerable<ProcessMapReportDto> HandleSection(
            IEnumerable<WellOperationDto> sectionOperations,
            IEnumerable<ProcessMapDto> sectionProcessMap,
            IEnumerable<TelemetryDataSaubStatDto> telemetryDataStat,
            IEnumerable<LimitingParameterDataDto> limitingParameters,
            IEnumerable<SubsystemOperationTimeDto> subsystemsOperationTime)
        {
            var minDepth = sectionOperations.Min(o => o.DepthStart);
            var maxDepth = sectionOperations.Max(o => o.DepthEnd);

            var depthIntervals = SplitByIntervals(minDepth, maxDepth).ToArray();
            var result = new ProcessMapReportDto[depthIntervals.Length];

            for (var i = 0; i < depthIntervals.Length; i++ )
                result[i] = MakeProcessMapReportDto(depthIntervals[i],  sectionOperations, sectionProcessMap, telemetryDataStat, limitingParameters, subsystemsOperationTime);
            
            return result;
        }

        private static ProcessMapReportDto MakeProcessMapReportDto(
            (double min, double max) depthInterval,
            IEnumerable<WellOperationDto> sectionOperations,
            IEnumerable<ProcessMapDto> sectionProcessMap,
            IEnumerable<TelemetryDataSaubStatDto> telemetryDataStat,
            IEnumerable<LimitingParameterDataDto> limitingParameters,
            IEnumerable<SubsystemOperationTimeDto> subsystemsOperationTime)
        {
            var dto = new ProcessMapReportDto{
                DepthStart = depthInterval.min
            };

            // TODO: trim items by detpth intervals. Use linear interpolation.            
            var intervalOperations = sectionOperations.Where(o => o.DepthEnd >= depthInterval.min && o.DepthStart <= depthInterval.max);
            var intervalProcessMap = sectionProcessMap.Where(map => map.DepthEnd >= depthInterval.min && map.DepthStart <= depthInterval.max);
            var intervalTelemetryDataStat = CalcIntervalTelemetryDataStat(depthInterval, telemetryDataStat);
            var intervalLimitingParametrs = limitingParameters.Where(l => l.DepthEnd >= depthInterval.min && l.DepthStart <= depthInterval.max);
            var intervalSubsystemsOperationTime = subsystemsOperationTime.Where(o => o.DepthEnd >= depthInterval.min && o.DepthStart <= depthInterval.max);

            var firstIntervalOperation = intervalOperations.FirstOrDefault();
            if (firstIntervalOperation is not null)
            { 
                dto.DateStart = GetInterpolatedDate(firstIntervalOperation, depthInterval.min);
                dto.IdWell = firstIntervalOperation.IdWell;
                dto.IdWellSectionType = firstIntervalOperation.IdWellSectionType;
                dto.WellSectionTypeName = firstIntervalOperation.WellSectionTypeName;
                dto.MechDrillingHours = CalcHours(depthInterval, intervalOperations);
            }

            // TODO: Разделить интервальные коллекции на ротор и слайд. Пока нет готовой методики.
            var slideOperations = intervalOperations.Where(o => o.IdCategory == WellOperationCategory.IdSlide);
            var rotorOperations = intervalOperations.Where(o => o.IdCategory == WellOperationCategory.IdRotor);
            
            dto.Slide = CalcDrillModeStat(depthInterval, slideOperations, intervalProcessMap, intervalTelemetryDataStat, intervalLimitingParametrs, intervalSubsystemsOperationTime);
            dto.Rotor = CalcDrillModeStat(depthInterval, rotorOperations, intervalProcessMap, intervalTelemetryDataStat, intervalLimitingParametrs, intervalSubsystemsOperationTime);
            
            return dto;
        }

        private static TelemetryDataSaubStatDto? CalcIntervalTelemetryDataStat((double min, double max) depthInterval, IEnumerable<TelemetryDataSaubStatDto> telemetryDataStat)
        {
            TelemetryDataSaubStatDto[] data = telemetryDataStat
                .Where(d => d.WellDepthMin <= depthInterval.max && d.WellDepthMax >= depthInterval.min)
                .ToArray();

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

            if (data.Length == 1)
                return data.First();

            var result = new TelemetryDataSaubStatDto
            {
                WellDepthMin = data.Min(d => d.WellDepthMin),
                WellDepthMax = data.Max(d => d.WellDepthMax),
                DateMin = data.Min(d => d.DateMin),
                DateMax = data.Max(d => d.DateMax),
            };

            var intervalDeltaDepth = result.WellDepthMax - result.WellDepthMin;

            foreach (var item in data)
            {
                var itemWeight = (item.WellDepthMax - item.WellDepthMin) / intervalDeltaDepth;

                result.Pressure += item.Pressure * itemWeight;
                result.PressureSp += item.PressureSp * itemWeight;
                result.PressureSpRotor += item.PressureSpSlide * itemWeight;
                result.PressureIdle += item.PressureIdle * itemWeight;
                result.PressureDelta += item.PressureDelta * itemWeight;

                result.AxialLoad += item.AxialLoad * itemWeight;
                result.AxialLoadSp += item.AxialLoadSp * itemWeight;
                result.AxialLoadLimitMax += item.AxialLoadLimitMax * itemWeight;

                result.RotorTorque += item.RotorTorque * itemWeight;
                result.RotorTorqueSp += item.RotorTorqueSp * itemWeight;
                result.RotorTorqueLimitMax += item.RotorTorqueLimitMax * itemWeight;

                result.BlockSpeed += item.BlockSpeed * itemWeight;
                result.BlockSpeedSp += item.BlockSpeedSp * itemWeight;
                result.BlockSpeedSpRotor += item.BlockSpeedSpRotor * itemWeight;
                result.BlockSpeedSpSlide += item.BlockSpeedSpSlide * itemWeight;
            }

            return result;
        }

        private static ProcessMapReportRowDto CalcDrillModeStat(
            (double min, double max) depthInterval, 
            IEnumerable<WellOperationDto> intervalModeOperations, 
            IEnumerable<ProcessMapDto> intervalProcessMap,
            TelemetryDataSaubStatDto? telemetryDataStat,
            IEnumerable<LimitingParameterDataDto> intervalLimitingParametrs,
            IEnumerable<SubsystemOperationTimeDto> intervalSubsystemsOperationTime)
        {
            var dto = new ProcessMapReportRowDto();
            if (intervalModeOperations.Any())
            {
                var deltaDepth = CalcDeltaDepth(depthInterval, intervalModeOperations);
                dto.DeltaDepth = deltaDepth;
                dto.Rop = deltaDepth / CalcHours(depthInterval, intervalModeOperations);                    
            };

            if (intervalProcessMap.Any())
            {
                var processMapFirst = intervalProcessMap.First();
                dto.PressureDiff.SetpointPlan = processMapFirst.Pressure.Plan;
                dto.AxialLoad.SetpointPlan = processMapFirst.AxialLoad.Plan;
                dto.TopDriveTorque.SetpointPlan = processMapFirst.TopDriveTorque.Plan;
                //dto.SpeedLimit.SetpointPlan = null;
            }

            if (telemetryDataStat is not null)
            {
                dto.PressureDiff.SetpointFact = telemetryDataStat.PressureSp;
                dto.PressureDiff.Fact = telemetryDataStat.PressureDelta;
                dto.PressureDiff.Limit = telemetryDataStat.PressureDeltaLimitMax;

                dto.AxialLoad.SetpointFact = telemetryDataStat.AxialLoadSp;
                dto.AxialLoad.Fact = telemetryDataStat.AxialLoad;
                dto.AxialLoad.Limit = telemetryDataStat.AxialLoadLimitMax;

                dto.TopDriveTorque.SetpointFact = telemetryDataStat.RotorTorqueSp;
                dto.TopDriveTorque.Fact = telemetryDataStat.RotorTorque;
                dto.TopDriveTorque.Limit = telemetryDataStat.RotorTorqueLimitMax;

                dto.SpeedLimit.SetpointFact = telemetryDataStat.BlockSpeedSp;
                dto.SpeedLimit.Fact = telemetryDataStat.BlockSpeed;
                //dto.SpeedLimit.Limit = mull;
            }

            if(intervalLimitingParametrs.Any()) 
            {
                const int idLimParamRop = 1;
                const int idLimParamPressure = 2;
                const int idLimParamAxialLoad = 3;
                const int idLimParamTorque = 4;

                var intervalLimitingParametrsStat = intervalLimitingParametrs
                    .GroupBy(p => p.IdFeedRegulator)
                    .Select(g => new
                    {
                        IdLimParam = g.Key,
                        SumDepth = g.Sum(p => p.DepthEnd - p.DepthStart),
                    });

                var totalDepth = intervalLimitingParametrsStat
                    .Sum(s => s.SumDepth);

                if (totalDepth > 0)
                {
                    dto.AxialLoad.PercDrillingSetpoint = intervalLimitingParametrsStat
                        .FirstOrDefault(s => s.IdLimParam == idLimParamAxialLoad)?.SumDepth / totalDepth;

                    dto.PressureDiff.PercDrillingSetpoint = intervalLimitingParametrsStat
                        .FirstOrDefault(s => s.IdLimParam == idLimParamPressure)?.SumDepth / totalDepth;

                    dto.TopDriveTorque.PercDrillingSetpoint = intervalLimitingParametrsStat
                        .FirstOrDefault(s => s.IdLimParam == idLimParamTorque)?.SumDepth / totalDepth;

                    dto.SpeedLimit.PercDrillingSetpoint = intervalLimitingParametrsStat
                        .FirstOrDefault(s => s.IdLimParam == idLimParamRop)?.SumDepth / totalDepth;
                }
            }

            if (intervalSubsystemsOperationTime.Any() && dto.DeltaDepth > 0)
            {
                dto.Usage = intervalSubsystemsOperationTime.Sum(t => t.DepthEnd - t.DepthStart) / dto.DeltaDepth.Value;
            }

            return dto;
        }

        private static double CalcDeltaDepth((double min, double max) depthInterval, IEnumerable<WellOperationDto> intervalOperations)
        {
            var ddepth = 0d;
            foreach (var operation in intervalOperations) 
            {
                var depthStart = operation.DepthStart > depthInterval.min
                    ? operation.DepthStart
                    : depthInterval.min;

                var depthEnd = operation.DepthEnd < depthInterval.max
                    ? operation.DepthEnd
                    : depthInterval.max;

                ddepth += (depthEnd - depthEnd);
            }
            return ddepth;
        }

        private static double CalcHours((double min, double max) depthInterval, IEnumerable<WellOperationDto> intervalOperations)
        {
            var hours = 0d;
            foreach (var operation in intervalOperations)
            {
                var dateStart = operation.DepthStart > depthInterval.min
                    ? operation.DateStart
                    : GetInterpolatedDate(operation, depthInterval.min);

                var dateEnd = operation.DepthEnd < depthInterval.max
                    ? operation.DateStart + TimeSpan.FromHours(operation.DurationHours)
                    : GetInterpolatedDate(operation, depthInterval.max);

                hours += (dateEnd - dateStart).TotalHours;
            }
            return hours;            
        }

        private static DateTime GetInterpolatedDate(WellOperationDto operation, double depth)
        {
            var ratio = (depth - operation.DepthStart) / (operation.DepthEnd - operation.DepthStart);
            var deltaHours = operation.DurationHours * ratio;
            var interpolatedDate = operation.DateStart + TimeSpan.FromHours(deltaHours);
            return interpolatedDate;
        }

        private static IEnumerable<(double min, double max)> SplitByIntervals(double min, double max)
        {
            const double step = 100;
            var iMin = min;
            var iMax = (1 + (int)(min / step)) * step;
            for (; iMax < max; iMax += step)
            {
                yield return (iMin, iMax);
                iMin = iMax;
            }
            yield return (iMin, max);
        }
    }
#nullable disable
}