using System;
using AsbCloudApp.Data;
using AsbCloudApp.Data.DetectedOperation;
using AsbCloudApp.Repositories;
using AsbCloudApp.Requests;
using AsbCloudApp.Services;
using AsbCloudDb.Model;
using Mapster;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AsbCloudApp.Data.WellOperation;
using AsbCloudApp.Exceptions;
using AsbCloudInfrastructure.Services.DetectOperations.Detectors;

namespace AsbCloudInfrastructure.Services.DetectOperations;

public class DetectedOperationService : IDetectedOperationService
{
    private readonly IDetectedOperationRepository operationRepository;
    private readonly IWellOperationCategoryRepository wellOperationCategoryRepository;
    private readonly IWellService wellService;
    private readonly ITelemetryService telemetryService;
    private readonly IRepositoryWellRelated<OperationValueDto> operationValueRepository;
    private readonly IScheduleRepository scheduleRepository;
    private readonly ITelemetryDataSaubService telemetryDataSaubService;

    private static readonly DetectorAbstract[] detectors = {
        new DetectorDrilling(),
        new DetectorSlipsTime(),
        new DetectorFlashing(),
        new DetectorConditioning(),
    };
    
    public DetectedOperationService(
        IDetectedOperationRepository operationRepository,
        IWellOperationCategoryRepository wellOperationCategoryRepository,
        IWellService wellService,
        ITelemetryService telemetryService,
        IRepositoryWellRelated<OperationValueDto> operationValueRepository,
        IScheduleRepository scheduleRepository,
        ITelemetryDataSaubService telemetryDataSaubService)
    {
        this.operationRepository = operationRepository;
        this.wellOperationCategoryRepository = wellOperationCategoryRepository;
        this.wellService = wellService;
        this.telemetryService = telemetryService;
        this.operationValueRepository = operationValueRepository;
        this.scheduleRepository = scheduleRepository;
        this.telemetryDataSaubService = telemetryDataSaubService;
    }

    public async Task<DetectedOperationListDto> GetAsync(DetectedOperationByWellRequest request, CancellationToken token)
    {
        var dtos = await GetOperationsAsync(request, token);
        if (dtos?.Any() != true)
            return new DetectedOperationListDto();

        var stats = GetOperationsDrillersStat(dtos);
        var result = new DetectedOperationListDto
        {
            Operations = dtos,
            Stats = stats
        };
        return result;
    }

    public async Task<IEnumerable<DetectedOperationWithDrillerDto>> GetOperationsAsync(DetectedOperationByWellRequest request, CancellationToken token)
    {
        var well = await wellService.GetOrDefaultAsync(request.IdWell, token);
        if (well?.IdTelemetry is null)
            return Enumerable.Empty<DetectedOperationWithDrillerDto>();

        var requestByTelemetry = new DetectedOperationByTelemetryRequest(well.IdTelemetry.Value, request);
        var data = await operationRepository.Get(requestByTelemetry, token);

        var operationValues = await operationValueRepository.GetByIdWellAsync(request.IdWell, token);
        var schedules = await scheduleRepository.GetByIdWellAsync(request.IdWell, token);
        var dtos = data.Select(o => Convert(o, operationValues, schedules));
        return dtos;
    }

    public async Task<int> InsertRangeManualAsync(int idEditor, int idWell, IEnumerable<DetectedOperationDto> dtos, CancellationToken token)
    {
        var idTelemetry = await GetIdTelemetryByWell(idWell, token);
        
        foreach (var dto in dtos)
        {
            dto.IdEditor = idEditor;
            dto.IdTelemetry = idTelemetry;
        }

        return await operationRepository.InsertRangeAsync(dtos, token);
    }

    public async Task<int> UpdateRangeManualAsync(int idEditor, int idWell, IEnumerable<DetectedOperationDto> dtos, CancellationToken token)
    {
        var idTelemetry = await GetIdTelemetryByWell(idWell, token);
        
        foreach (var dto in dtos)
        {
            dto.IdEditor = idEditor;
            dto.IdTelemetry = idTelemetry;
        }

        return await operationRepository.UpdateRangeAsync(dtos, token);
    }
    
    private async Task<int> GetIdTelemetryByWell(int idWell, CancellationToken token)
    {
        var well = await wellService.GetOrDefaultAsync(idWell, token) ??
                   throw new ArgumentInvalidException(nameof(idWell), "Well doesn't exist");

        var idTelemetry = well.IdTelemetry ??
                          throw new ArgumentInvalidException(nameof(idWell), "У скважины отсутствует телеметрия");

        return idTelemetry;
    }

    public async Task<IEnumerable<WellOperationCategoryDto>> GetCategoriesAsync(int? idWell, CancellationToken token)
    {
        if(idWell is null)
        {
            return wellOperationCategoryRepository.Get(false);
        }
        else
        {
            var well = await wellService.GetOrDefaultAsync((int )idWell, token);
            if (well?.IdTelemetry is null)
                return Enumerable.Empty<WellOperationCategoryDto>();

            var request = new DetectedOperationByTelemetryRequest()
            {
                IdTelemetry = well.IdTelemetry.Value
            };

            var operations = await operationRepository.Get(request, token);
            var categories = operations
                .Select(o => o.OperationCategory)
                .Distinct();
            return categories;
        }
    }

    [Obsolete]
    public async Task<IEnumerable<DetectedOperationStatDto>> GetOperationsStatAsync(DetectedOperationByWellRequest request, CancellationToken token)
    {
        var well = await wellService.GetOrDefaultAsync(request.IdWell, token);
        if (well?.IdTelemetry is null || well.Timezone is null)
            return Enumerable.Empty<DetectedOperationStatDto>();

        var requestByTelemetry = new DetectedOperationByTelemetryRequest(well.IdTelemetry.Value, request);

        var operations = await operationRepository.Get(requestByTelemetry, token);

        if (!operations.Any())
            return Enumerable.Empty<DetectedOperationStatDto>();

        var dtos = operations
            .GroupBy(o => (o.IdCategory, o.OperationCategory.Name))
            .OrderBy(g => g.Key)
            .Select(g => new DetectedOperationStatDto
            {
                IdCategory = g.Key.IdCategory,
                Category = g.Key.Name,
                Count = g.Count(),
                MinutesAverage = g.Average(o => o.DurationMinutes),
                MinutesMin = g.Min(o => o.DurationMinutes),
                MinutesMax = g.Max(o => o.DurationMinutes),
                MinutesTotal = g.Sum(o => o.DurationMinutes),
                ValueAverage = g.Average(o => o.Value),
                ValueMax = g.Max(o => o.Value),
                ValueMin = g.Min(o => o.Value),
            });

        return dtos;
    }

    public async Task<(DateTimeOffset LastDate, IEnumerable<DetectedOperationDto> Items)> DetectOperationsAsync(int idTelemetry,
        TelemetryDataRequest request,
        DetectedOperationDto? lastDetectedOperation,
        CancellationToken token)
    {
        const int minOperationLength = 5;
        const int maxDetectorsInterpolationFrameLength = 30;
        const int gap = maxDetectorsInterpolationFrameLength + minOperationLength;

        var telemetries = await telemetryDataSaubService.GetByTelemetryAsync(idTelemetry, request, token);

        var count = telemetries.Count();

        if (count == 0)
            throw new InvalidOperationException("InvalidOperation_EmptyTelemetries");

        var timeZone = telemetryService.GetTimezone(idTelemetry);

        var detectedOperations = new List<DetectedOperationDto>();

        var detectableTelemetries = telemetries
            .Where(t => t.BlockPosition >= 0)
            .Select(t => new DetectableTelemetry
            {
                DateTime = new DateTimeOffset(t.DateTime, timeZone.Offset),
                IdUser = t.IdUser,
                Mode = t.Mode,
                WellDepth = t.WellDepth,
                Pressure = t.Pressure,
                HookWeight = t.HookWeight,
                BlockPosition = t.BlockPosition,
                BitDepth = t.BitDepth,
                RotorSpeed = t.RotorSpeed,
                AxialLoad = t.AxialLoad,
            }).ToArray();
        
        if (detectableTelemetries.Length <= gap)
        {
            var lastTelemetry = telemetries.Last();
            var lastDateTelemetry = new DateTimeOffset(lastTelemetry.DateTime, timeZone.Offset);
            return (lastDateTelemetry, Enumerable.Empty<DetectedOperationDto>());
        }

        var positionBegin = 0;
        var positionEnd = detectableTelemetries.Length - gap;

        while (positionEnd > positionBegin)
        {
            foreach (var detector in detectors)
            {
                if (!detector.TryDetect(idTelemetry, detectableTelemetries, positionBegin, positionEnd, lastDetectedOperation,
                        out var result))
                    continue;

                detectedOperations.Add(result!.Operation);
                lastDetectedOperation = result.Operation;
                positionBegin = result.TelemetryEnd;
                break;
            }

            var point0 = detectableTelemetries[positionBegin];
            
            while (positionBegin < positionEnd && IsChangingTelemetryInterval(point0, detectableTelemetries[positionBegin]))
                positionBegin++;
        }

        return (detectableTelemetries[positionBegin].DateTime, detectedOperations);
    }

    public async Task<int> DeleteAsync(DetectedOperationByWellRequest request, CancellationToken token)
    {
        var well = await wellService.GetOrDefaultAsync(request.IdWell, token);
        if (well?.IdTelemetry is null || well.Timezone is null)
            return 0;

        var requestByTelemetry = new DetectedOperationByTelemetryRequest(well.IdTelemetry.Value, request);
        var result = await operationRepository.DeleteAsync(requestByTelemetry, token);
        return result;
    }

    private static bool IsChangingTelemetryInterval(DetectableTelemetry telemetryBegin, DetectableTelemetry telemetryEnd)
    {
        return telemetryBegin.Mode == telemetryEnd.Mode &&
               EqualParameter(telemetryBegin.WellDepth, telemetryEnd.WellDepth, 0.01f) &&
               EqualParameter(telemetryBegin.Pressure, telemetryEnd.Pressure, 0.1f) &&
               EqualParameter(telemetryBegin.HookWeight, telemetryEnd.HookWeight, 0.1f) &&
               EqualParameter(telemetryBegin.BlockPosition, telemetryEnd.BlockPosition, 0.01f) &&
               EqualParameter(telemetryBegin.BitDepth, telemetryEnd.BitDepth, 0.01f) &&
               EqualParameter(telemetryBegin.RotorSpeed, telemetryEnd.RotorSpeed, 0.01f) &&
               EqualParameter(telemetryBegin.AxialLoad, telemetryEnd.AxialLoad, 0.1f);

        static bool EqualParameter(float value, float origin, float tolerance)
        {
            return value <= origin + tolerance && value >= origin - tolerance;
        }
    } 

    private static IEnumerable<DetectedOperationDrillersStatDto> GetOperationsDrillersStat(IEnumerable<DetectedOperationWithDrillerDto> operations)
    {
        var groups = operations.GroupBy(o => o.Driller);

        var stats = new List<DetectedOperationDrillersStatDto>(groups.Count());
        foreach (var group in groups)
        {
            var itemsWithTarget = group.Where(i => i.OperationValue is not null);
            var stat = new DetectedOperationDrillersStatDto
            {
                Driller = group.Key,
                AverageValue = group.Sum(e => e.Value) / group.Count(),
                Count = group.Count(),
            };
            if (itemsWithTarget.Any())
            {
                var itemsOutOfTarget = itemsWithTarget.Where(o => !IsTargetOk(o));
                stat.AverageTargetValue = itemsWithTarget.Average(e => e.OperationValue?.TargetValue);
                stat.Efficiency = 100d * itemsOutOfTarget.Count() / itemsWithTarget.Count();
                stat.Loss = itemsOutOfTarget.Sum(DeltaToTarget);
            }

            stats.Add(stat);
        }
        return stats;
    }

    private static bool IsTargetOk(DetectedOperationWithDrillerDto op)
    {
        return (op.IdCategory) switch
        {
            WellOperationCategory.IdRotor => op.Value > op.OperationValue?.TargetValue,
            WellOperationCategory.IdSlide => op.Value > op.OperationValue?.TargetValue,
            WellOperationCategory.IdSlipsTime => op.Value > op.OperationValue?.TargetValue,
            _ => op.Value > op.OperationValue?.TargetValue,
        };
    }

    private static double DeltaToTarget(DetectedOperationWithDrillerDto op)
    {
        return (op.IdCategory) switch
        {
            WellOperationCategory.IdRotor => 0,
            WellOperationCategory.IdSlide => 0,
            WellOperationCategory.IdSlipsTime => op.Value - op.OperationValue?.TargetValue??0,
            _ => 0,
        };
    }

    private static DetectedOperationWithDrillerDto Convert(DetectedOperationDto operation, IEnumerable<OperationValueDto> operationValues, IEnumerable<ScheduleDto> schedules)
    {
        var dto = operation.Adapt<DetectedOperationWithDrillerDto>();
        dto.OperationValue = operationValues.FirstOrDefault(v => v.IdOperationCategory == dto.IdCategory
              && v.DepthStart <= dto.DepthStart
              && v.DepthEnd > dto.DepthStart);
        
        var dateStart = dto.DateStart.ToUniversalTime();
        var timeStart = new TimeDto(dateStart);
        var driller = schedules.FirstOrDefault(s =>
            s.DrillStart <= dateStart &&
            s.DrillEnd > dateStart && (
            s.ShiftStart > s.ShiftEnd
            ) ^ (s.ShiftStart <= timeStart &&
            s.ShiftEnd > timeStart
            ))
            ?.Driller;
        dto.Driller = driller;

        return dto;
    }
}