using AsbCloudApp.Data;
using AsbCloudApp.Data.ProcessMaps;
using AsbCloudApp.Data.ProcessMaps.Report;
using AsbCloudApp.Exceptions;
using AsbCloudApp.Extensions;
using AsbCloudApp.Repositories;
using AsbCloudApp.Requests;
using AsbCloudApp.Services;
using AsbCloudApp.Services.ProcessMaps.WellDrilling;
using AsbCloudDb.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace AsbCloudInfrastructure.Services.ProcessMaps.Report;

public class ProcessMapReportDrillingService : IProcessMapReportDrillingService
{
    private readonly IWellService wellService;
    private readonly IChangeLogRepository<ProcessMapPlanDrillingDto, ProcessMapPlanBaseRequestWithWell> processMapPlanBaseRepository;
    private readonly IDataSaubStatRepository dataSaubStatRepository;
    private readonly IWellOperationRepository wellOperationRepository;
    private readonly IWellOperationCategoryRepository wellOperationCategoryRepository;

    public ProcessMapReportDrillingService(IWellService wellService,
        IChangeLogRepository<ProcessMapPlanDrillingDto, ProcessMapPlanBaseRequestWithWell> processMapPlanBaseRepository,
        IDataSaubStatRepository dataSaubStatRepository,
        IWellOperationRepository wellOperationRepository,
        IWellOperationCategoryRepository wellOperationCategoryRepository
        )
    {
        this.wellService = wellService;
        this.processMapPlanBaseRepository = processMapPlanBaseRepository;
        this.dataSaubStatRepository = dataSaubStatRepository;
        this.wellOperationRepository = wellOperationRepository;
        this.wellOperationCategoryRepository = wellOperationCategoryRepository;
    }

    public async Task<IEnumerable<ProcessMapReportDataSaubStatDto>> GetAsync(int idWell, DataSaubStatRequest request, CancellationToken token)
    {
        var well = await wellService.GetOrDefaultAsync(idWell, token)
               ?? throw new ArgumentInvalidException(nameof(idWell), $"Скважина с Id: {idWell} не найдена");

        if (!well.IdTelemetry.HasValue)
            return Enumerable.Empty<ProcessMapReportDataSaubStatDto>();

        var requestProcessMapPlan = new ProcessMapPlanBaseRequestWithWell(idWell);
        var processMapPlanWellDrillings = await processMapPlanBaseRepository.Get(requestProcessMapPlan, token);

        if (!processMapPlanWellDrillings.Any())
            return Enumerable.Empty<ProcessMapReportDataSaubStatDto>();

        var geDepth = processMapPlanWellDrillings.Min(p => p.DepthStart);
        var leDepth = processMapPlanWellDrillings.Max(p => p.DepthEnd);

        var requestWellOperationFact = new WellOperationRequest()
        {
            IdWell = idWell,
            OperationType = WellOperation.IdOperationTypeFact,
            GeDepth = geDepth,
            LeDepth = leDepth
        };
        var wellOperations = await wellOperationRepository
            .GetAsync(requestWellOperationFact, token);
        if (!wellOperations.Any())
            return Enumerable.Empty<ProcessMapReportDataSaubStatDto>();

        var geDate = wellOperations.Min(p => p.DateStart);
        var leDate = wellOperations.Max(p => (p.DateStart.AddHours(p.DurationHours)));
        var dataSaubStats =
            (await dataSaubStatRepository.GetAsync(well.IdTelemetry.Value, geDate, leDate, token)).ToArray();

        if (!dataSaubStats.Any())
            return Enumerable.Empty<ProcessMapReportDataSaubStatDto>();

        var wellOperationCategories = wellOperationCategoryRepository.Get(false);
        var wellSectionTypes = wellOperationRepository.GetSectionTypes();

        var result = CalcByIntervals(
            request, 
            processMapPlanWellDrillings, 
            dataSaubStats, 
            wellOperations, 
            wellOperationCategories,
            wellSectionTypes);

        return result;
    }

    private static IEnumerable<ProcessMapReportDataSaubStatDto> CalcByIntervals(
        DataSaubStatRequest request,
        IEnumerable<ProcessMapPlanDrillingDto> processMapPlanWellDrillings,
        Span<DataSaubStatDto> dataSaubStats,
        IEnumerable<WellOperationDto> wellOperations,
        IEnumerable<WellOperationCategoryDto> wellOperationCategories,
        IEnumerable<WellSectionTypeDto> wellSectionTypes
    )
    {
        var list = new List<ProcessMapReportDataSaubStatDto>();
        var firstElemInInterval = dataSaubStats[0];

        int GetSection(DataSaubStatDto data) 
            => wellOperations.MinBy(o => data.DateStart - o.DateStart)!.IdWellSectionType;

        ProcessMapPlanDrillingDto? GetProcessMapPlan(int idWellSectionType, DataSaubStatDto data) 
            => processMapPlanWellDrillings
                .Where(p => p.IdWellSectionType == idWellSectionType)
                .Where(p => p.DepthStart <= data.DepthStart)
                .Where(p => p.DepthEnd >= data.DepthStart)
                .Where(p => IsModeMatchOperationCategory(p.IdMode, data.IdCategory))
                .WhereActualAtMoment(data.DateStart)
                .FirstOrDefault();

        var idWellSectionType = GetSection(firstElemInInterval);
        var prevProcessMapPlan = GetProcessMapPlan(idWellSectionType, firstElemInInterval);
        var indexStart = 0;

        for (var i = 1; i < dataSaubStats.Length; i++)
        {
            var currentElem = dataSaubStats[i];
            idWellSectionType = GetSection(currentElem);
            var processMapPlan = GetProcessMapPlan(idWellSectionType, currentElem);

            if (IsNewInterval(currentElem, firstElemInInterval, request) || i == dataSaubStats.Length - 1 || processMapPlan != prevProcessMapPlan)
            {
                prevProcessMapPlan = processMapPlan;
                var length = i - indexStart;

                var span = dataSaubStats.Slice(indexStart, length);

                indexStart = i;
                firstElemInInterval = currentElem;

                var firstElemInSpan = span[0];
                var lastElemInISpan = span[^1];

                var wellOperationCategoryName = wellOperationCategories
                    .Where(c => c.Id == firstElemInSpan.IdCategory)
                    .FirstOrDefault()?.Name ?? string.Empty;

                var wellSectionType = wellSectionTypes
                    .Where(c => c.Id == idWellSectionType)
                    .First();

                var elem = CalcStat(processMapPlan, span, wellOperationCategoryName, wellSectionType);
                if (elem is not null)
                    list.Add(elem);                    
            }
        }
        return list;
    }

    private static bool IsModeMatchOperationCategory(int idMode, int idCategory)
    {
        return (idMode == 1 && idCategory == 5003) || (idMode == 2 && idCategory == 5002);
    }

    private static ProcessMapReportDataSaubStatDto? CalcStat(
        ProcessMapPlanDrillingDto? processMapPlanFilteredByDepth,
        Span<DataSaubStatDto> span,
        string wellOperationCategoryName,
        WellSectionTypeDto wellSectionType
    )
    {
        var firstElemInInterval = span[0];
        var lastElemInInterval = span[^1];

        var deltaDepth = lastElemInInterval.DepthEnd - firstElemInInterval.DepthStart;

        var aggregatedValues = CalcAggregate(span);

        var result = new ProcessMapReportDataSaubStatDto()
        {
            IdWellSectionType = wellSectionType.Id,
            DateStart = firstElemInInterval.DateStart.DateTime,
            WellSectionTypeName = wellSectionType.Caption,
            DepthStart = firstElemInInterval.DepthStart,
            DepthEnd = lastElemInInterval.DepthEnd,
            DeltaDepth = deltaDepth,
            DrilledTime = aggregatedValues.DrilledTime,
            DrillingMode = wellOperationCategoryName,
            PressureDiff = new ProcessMapReportDataSaubStatParamsDto()
            {
                SetpointPlan = processMapPlanFilteredByDepth?.DeltaPressurePlan,
                SetpointFact = firstElemInInterval.PressureSp - firstElemInInterval.PressureIdle,
                FactWavg = aggregatedValues.Pressure,
                Limit = processMapPlanFilteredByDepth?.DeltaPressureLimitMax,
                SetpointUsage = aggregatedValues.SetpointUsagePressure
            },
            AxialLoad = new ProcessMapReportDataSaubStatParamsDto()
            {
                SetpointPlan = processMapPlanFilteredByDepth?.AxialLoadPlan,
                SetpointFact = aggregatedValues.AxialLoadSp,
                FactWavg = aggregatedValues.AxialLoad,
                Limit = processMapPlanFilteredByDepth?.AxialLoadLimitMax,
                SetpointUsage = aggregatedValues.SetpointUsageAxialLoad
            },
            TopDriveTorque = new ProcessMapReportDataSaubStatParamsDto()
            {
                SetpointPlan = processMapPlanFilteredByDepth?.TopDriveTorquePlan,
                SetpointFact = aggregatedValues.RotorTorqueSp,
                FactWavg = aggregatedValues.RotorTorque,
                FactMax = aggregatedValues.RotorTorqueMax,
                Limit = processMapPlanFilteredByDepth?.TopDriveTorqueLimitMax,
                SetpointUsage = aggregatedValues.SetpointUsageRotorTorque
            },
            SpeedLimit = new ProcessMapReportDataSaubStatParamsDto
            {
                SetpointPlan = processMapPlanFilteredByDepth?.RopPlan,
                SetpointFact = aggregatedValues.BlockSpeedSp,
                FactWavg = deltaDepth / aggregatedValues.DrilledTime,
                SetpointUsage = aggregatedValues.SetpointUsageRopPlan
            },
            TopDriveSpeed = new ProcessMapReportDataSaubStatParamsDto
            {
                SetpointPlan = processMapPlanFilteredByDepth?.TopDriveSpeedPlan,
                FactWavg = aggregatedValues.RotorSpeed,
                FactMax = aggregatedValues.RotorSpeedMax
            },
            Flow = new ProcessMapReportDataSaubStatParamsDto
            {
                SetpointPlan = processMapPlanFilteredByDepth?.FlowPlan,
                FactWavg = aggregatedValues.MaxFlow,
                Limit = processMapPlanFilteredByDepth?.FlowLimitMax,
            },
            Rop = new PlanFactDto<double?>
            {
                Plan = processMapPlanFilteredByDepth?.RopPlan,
                Fact = deltaDepth / aggregatedValues.DrilledTime
            },
        };
        return result;
    }

    private static (
        double Pressure,
        double AxialLoadSp,
        double AxialLoad,
        double RotorTorqueSp,
        double RotorTorque,
        double RotorTorqueMax,
        double BlockSpeedSp,
        double RotorSpeed,
        double RotorSpeedMax,
        double MaxFlow,
        double SetpointUsagePressure,
        double SetpointUsageAxialLoad,
        double SetpointUsageRotorTorque,
        double SetpointUsageRopPlan,
        double DrilledTime
        ) CalcAggregate(Span<DataSaubStatDto> span)
    {
        var sumPressure = 0.0;
        var sumAxialLoadSp = 0.0;
        var sumAxialLoad = 0.0;
        var sumRotorTorqueSp = 0.0;
        var sumRotorTorque = 0.0;
        var sumBlockSpeedSp = 0.0;
        var sumRotorSpeed = 0.0;
        var maxFlow = 0.0;
        var maxRotorTorque = 0.0;
        var maxRotorSpeed = 0.0;
        var sumDiffDepthByPressure = 0.0;
        var sumDiffDepthByAxialLoad = 0.0;
        var sumDiffDepthByRotorTorque = 0.0;
        var sumDiffDepthByRopPlan = 0.0;

        var diffDepthTotal = 0.0;
        var drilledTime = 0.0;

        for (var i = 0; i < span.Length; i++)
        {
            var diffDepth = span[i].DepthEnd - span[i].DepthStart;

            sumPressure += diffDepth * (span[i].Pressure - (span[i].PressureIdle ?? 0.0));
            sumAxialLoadSp += diffDepth * (span[i].AxialLoadSp ?? 0);
            sumAxialLoad += diffDepth * span[i].AxialLoad;
            sumRotorTorqueSp += diffDepth * (span[i].RotorTorqueSp ?? 0);
            sumRotorTorque += diffDepth * span[i].RotorTorque;
            sumBlockSpeedSp += diffDepth * (span[i].BlockSpeedSp ?? 0);
            sumRotorSpeed += diffDepth * span[i].RotorSpeed;
            maxFlow = span[i].Flow > maxFlow ? span[i].Flow : maxFlow;
            maxRotorTorque = span[i].RotorTorque > maxRotorTorque ? span[i].RotorTorque : maxRotorTorque;
            maxRotorSpeed = span[i].RotorSpeed > maxRotorSpeed ? span[i].RotorSpeed : maxRotorSpeed;

            if (span[i].IdFeedRegulator == LimitingParameterDto.Pressure)
                sumDiffDepthByPressure += diffDepth;
            if (span[i].IdFeedRegulator == LimitingParameterDto.AxialLoad)
                sumDiffDepthByAxialLoad += diffDepth;
            if (span[i].IdFeedRegulator == LimitingParameterDto.RotorTorque)
                sumDiffDepthByRotorTorque += diffDepth;
            if (span[i].IdFeedRegulator == LimitingParameterDto.RopPlan)
                sumDiffDepthByRopPlan += diffDepth;

            diffDepthTotal += diffDepth;
            drilledTime += (span[i].DateEnd - span[i].DateStart).TotalHours;
        }
        return (
            Pressure: sumPressure / diffDepthTotal,
            AxialLoadSp: sumAxialLoadSp / diffDepthTotal,
            AxialLoad: sumAxialLoad / diffDepthTotal,
            RotorTorqueSp: sumRotorTorqueSp / diffDepthTotal,
            RotorTorque: sumRotorTorque / diffDepthTotal,
            RotorTorqueMax: maxRotorTorque,
            BlockSpeedSp: sumBlockSpeedSp / diffDepthTotal,
            RotorSpeed: sumRotorSpeed / diffDepthTotal,
            RotorSpeedMax: maxRotorSpeed,
            MaxFlow: maxFlow,
            SetpointUsagePressure: sumDiffDepthByPressure * 100 / diffDepthTotal,
            SetpointUsageAxialLoad: sumDiffDepthByAxialLoad * 100 / diffDepthTotal,
            SetpointUsageRotorTorque: sumDiffDepthByRotorTorque * 100 / diffDepthTotal,
            SetpointUsageRopPlan: sumDiffDepthByRopPlan * 100/ diffDepthTotal,
            DrilledTime: drilledTime
        );
    }

    private static bool IsNewInterval(DataSaubStatDto currentElem, DataSaubStatDto firstElem, DataSaubStatRequest request)
    {
        static bool IsNewElemBySpeed(double currentSpeed, double firstSpeed)
        {
            //2. Изменение уставки скорости подачи от первого значения в начале интервала при условии:
            //скорость > 80 м/ч => изменение уставки на ± 20 м/ч;
            //скорость > 30 м/ч => изменение уставки на ± 15 м/ч;
            //скорость <= 30 м/ч => изменение уставки на ± 5 м/ч;
            if (firstSpeed > 80)
                return Math.Abs(currentSpeed - firstSpeed) >= 20;
            else if (firstSpeed > 30)
                return Math.Abs(currentSpeed - firstSpeed) >= 15;
            else
                return Math.Abs(currentSpeed - firstSpeed) >= 5;
        }

        var isNewElem = (currentElem.IdCategory != firstElem.IdCategory)
            || (Math.Abs(currentElem.Pressure - firstElem.Pressure) >= request.DeltaPressure)
            || (Math.Abs(currentElem.AxialLoad - firstElem.AxialLoad) >= request.DeltaAxialLoad)
            || (Math.Abs(currentElem.RotorTorque - firstElem.RotorTorque) >= request.DeltaRotorTorque)
            || (Math.Abs((currentElem.AxialLoadSp ?? 0) - (firstElem.AxialLoadSp ?? 0)) >= request.DeltaAxialLoadSp)
            || (Math.Abs((currentElem.RotorTorqueSp ?? 0) - (firstElem.RotorTorqueSp ?? 0)) >= request.DeltaRotorTorqueSp)
            || (IsNewElemBySpeed(currentElem.Speed, firstElem.Speed));
        return isNewElem;
    }
}