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

namespace AsbCloudInfrastructure.Services.Subsystems;

public class SubsystemService : ISubsystemService
{
    private const int IdSubsystemAPD = 1;
    private const int IdSubsystemAPDRotor = 11;
    private const int IdSubsystemAPDSlide = 12;
    private const int IdSubsystemOscillation = 65536;

    private readonly ICrudRepository<SubsystemDto> subsystemRepository;

    private readonly IWellService wellService;
    private readonly IDetectedOperationService detectedOperationService;
    private readonly IScheduleRepository scheduleRepository;
    private readonly ITelemetryDataSaubService telemetryDataSaubService;
    private readonly ITelemetryService telemetryService;
    private readonly IChangeLogRepository<ProcessMapPlanSubsystemsDto, ProcessMapPlanBaseRequestWithWell> processMapPlanRepository;

    private IDictionary<int, SubsystemDto> subsystems = new Dictionary<int, SubsystemDto>();

    public SubsystemService(ICrudRepository<SubsystemDto> subsystemRepository,
        IWellService wellService,
        IDetectedOperationService detectedOperationService,
        IScheduleRepository scheduleRepository,
        ITelemetryService telemetryService,
        ITelemetryDataSaubService telemetryDataSaubService,
        IChangeLogRepository<ProcessMapPlanSubsystemsDto, ProcessMapPlanBaseRequestWithWell> processMapPlanRepository)
    {
        this.wellService = wellService;
        this.subsystemRepository = subsystemRepository;
        this.detectedOperationService = detectedOperationService;
        this.scheduleRepository = scheduleRepository;
        this.telemetryService = telemetryService;
        this.telemetryDataSaubService = telemetryDataSaubService;
        this.processMapPlanRepository = processMapPlanRepository;
    }

    //  получить расписания бурильщиков по скважинам // ScheduleRepository добавить новый метод
    // [
    //      get telemetryId by idWell // IWellService.GetOrDefaultStatAsync
    //      получить detectedOperation by telemetry //detectedOperationService.GetOperationsAsync
    //      сгруппировать по бурильщикам
    //      [
    //         рассчитать статистику // CalcStatAsync
    //         join driller from group
    //      ]
    //      join well (cluster, deposit)
    // ]
    public async Task<IEnumerable<DrillerDetectedOperationStatDto>> GetByWellsAsync(GetStatRequest request,
        CancellationToken token)
    {
        var result = new List<DrillerDetectedOperationStatDto>();
        var schedulePage = await scheduleRepository.GetPageAsync(request, token);
        var wells = await wellService.GetAsync(new WellRequest { Ids = request.IdsWells }, token);

        foreach (var schedule in schedulePage)
        {
            var idWell = schedule.IdWell;
            var well = wells.FirstOrDefault(w => w.Id == idWell)!;

            var byWellRequest = new DetectedOperationByWellRequest(idWell, new DetectedOperationRequest());

            var detectedOperations = await detectedOperationService
                .GetOperationsAsync(byWellRequest, token);
            var detectedOperationsByCurrentDriller = detectedOperations.Where(d => d.Driller?.Id == schedule.IdDriller);

            var drillerOperationsStat = await CalcStatAsync(detectedOperationsByCurrentDriller, token);
            var dto = new DrillerDetectedOperationStatDto
            {
                Statistic = drillerOperationsStat,
                Schedule = schedule,
                Well = well,
            };
            result.Add(dto);
        }

        return result;
    }

    public async Task<IEnumerable<SubsystemStatDto>> GetStatAsync(SubsystemRequest request, CancellationToken token)
    {
        var well = await wellService.GetOrDefaultAsync(request.IdWell, token)
                   ?? throw new ArgumentInvalidException(nameof(request.IdWell),
                       $"Well Id: {request.IdWell} does not exist");

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

        var detectedOperationSummaryRequest = new DetectedOperationByWellRequest
        {
            IdWell = request.IdWell,
            IdsCategories = WellOperationCategory.MechanicalDrillingSubIds,

            GeDateStart = request.GeDate,
            LeDateEnd = request.LeDate,

            GeDepthStart = request.GeDepth,
            LeDepthEnd = request.LeDepth,
        };

        var operations = await detectedOperationService.GetOperationsAsync(detectedOperationSummaryRequest,
            token);

        if (request.IdDriller.HasValue)
            operations = operations.Where(o => o.Driller is not null && o.Driller.Id == request.IdDriller.Value);

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

        var stat = await CalcStatAsync(operations, token);
        return stat;
    }

    public async Task<IEnumerable<SubsystemActiveWellStatDto>> GetStatByActiveWells(IEnumerable<int> wellIds,
        CancellationToken token)
    {
        var activeWells = await wellService.GetAsync(new() { Ids = wellIds, IdState = 1 }, token);
        var result = await GetStatAsync(activeWells, null, null, token);
        return result;
    }

    public async Task<IEnumerable<SubsystemPlanFactStatDto>> GetStatPlanFactByWellsAsync(SubsystemPlanFactRequest request, CancellationToken token)
    {
        var telemetriesDict = telemetryService
            .GetOrDefaultTelemetriesByIdsWells(request.IdsWell)
            .ToDictionary(t => t.Id, t => t.IdWell);

        var geDate = request.GeDate ?? SubsystemPlanFactRequest.ValidationMinDate;
        var leDate = request.LeDate ?? DateTimeOffset.UtcNow;
        var groupedTelemetryDataSaub = await telemetryDataSaubService.GetMinAndMaxWellDepths(telemetriesDict.Keys, geDate, leDate, token);

        var result = new List<SubsystemPlanFactStatDto>();

        foreach (var telemetryDataSaubItem in groupedTelemetryDataSaub)
        {
            var idWell = telemetriesDict.GetValueOrDefault(telemetryDataSaubItem.Key);
            if (!idWell.HasValue)
                continue;

            var telemetryDataSaubInfo = telemetryDataSaubItem.Value;
            var requestProcessMapPlan = new ProcessMapPlanBaseRequestWithWell(idWell.Value, telemetryDataSaubInfo.MinDepth, telemetryDataSaubInfo.MaxDepth);
            var processMapPlanSubsystems = await processMapPlanRepository.GetCurrent(requestProcessMapPlan, token);

            foreach (var processMapPlanSubsystem in processMapPlanSubsystems)
            {
                var stat = new SubsystemPlanFactStatDto();
                stat.IdWell = processMapPlanSubsystem.IdWell;
                stat.IdWellSectionType = processMapPlanSubsystem.IdWellSectionType;
                stat.DepthStart = processMapPlanSubsystem.DepthStart < telemetryDataSaubInfo.MinDepth
                    ? telemetryDataSaubInfo.MinDepth
                    : processMapPlanSubsystem.DepthStart;
                stat.DepthEnd = processMapPlanSubsystem.DepthEnd > telemetryDataSaubInfo.MaxDepth
                    ? telemetryDataSaubInfo.MaxDepth
                    : processMapPlanSubsystem.DepthEnd;
                stat.AutoRotorPlan = processMapPlanSubsystem.AutoRotor;
                stat.AutoSlidePlan = processMapPlanSubsystem.AutoSlide;
                stat.AutoOscillationPlan = processMapPlanSubsystem.AutoOscillation;

                var subsystemRequest = new SubsystemRequest()
                {
                    IdWell = telemetriesDict.GetValueOrDefault(telemetryDataSaubItem.Key)!.Value,
                    GeDepth = stat.DepthStart,
                    LeDepth = stat.DepthEnd
                };
                var subsystemStatFact = await GetStatAsync(subsystemRequest, token);
                var subsystemStatFactDict = subsystemStatFact.ToDictionary(s => s.IdSubsystem);

                stat.AutoRotorFact = subsystemStatFactDict.GetValueOrDefault(IdSubsystemAPDRotor)?.KUsage * 100;
                stat.AutoSlideFact = subsystemStatFactDict.GetValueOrDefault(IdSubsystemAPDSlide)?.KUsage * 100;
                stat.AutoOscillationFact = subsystemStatFactDict.GetValueOrDefault(IdSubsystemOscillation)?.KUsage * 100;

                result.Add(stat);
            }
        }

        return result;
    }


    private async Task<IEnumerable<SubsystemStatDto>> CalcStatAsync(
        IEnumerable<DetectedOperationWithDrillerDto> operations, CancellationToken token)
    {
        if (!subsystems.Any())
            subsystems = (await subsystemRepository.GetAllAsync(token)).ToDictionary(s => s.Id, s => s);

        var oscillationStat = CalcOscillationStat(operations);
        var apdStat = CalcApdStat(operations);

        var stat = new List<SubsystemStatDto> { oscillationStat };
        stat.AddRange(apdStat);

        return stat;
    }

    private SubsystemStatDto CalcOscillationStat(IEnumerable<DetectedOperationWithDrillerDto> operations)
    {
        operations = operations.Where(o => o.IdCategory == WellOperationCategory.IdSlide);

        var (sumDepthInterval, usedTimeHours, operationCount) = AggregateOperations(IdSubsystemOscillation, operations);

        var oscillationStat = new SubsystemStatDto
        {
            IdSubsystem = IdSubsystemOscillation,
            SubsystemName = subsystems.TryGetValue(IdSubsystemOscillation, out var subsystemDto)
                ? subsystemDto.Name
                : "unknown",
            UsedTimeHours = usedTimeHours,
            SumOperationDepthInterval = operations.Sum(o => o.DepthEnd - o.DepthStart),
            SumOperationDurationHours = operations.Sum(o => o.DurationMinutes / 60),
            SumDepthInterval = sumDepthInterval,
            OperationCount = operationCount,
        };
        if (oscillationStat.SumOperationDepthInterval != 0d)
            oscillationStat.KUsage = oscillationStat.SumDepthInterval / oscillationStat.SumOperationDepthInterval;

        return oscillationStat;
    }

    private IEnumerable<SubsystemStatDto> CalcApdStat(IEnumerable<DetectedOperationWithDrillerDto> operations)
    {
        var apdRotorAndSlide = operations
            .Where(o => WellOperationCategory.MechanicalDrillingSubIds.Contains(o.IdCategory))
            .GroupBy(o => o.IdCategory)
            .Select(group =>
            {
                var idSubsystem = group.Key switch
                {
                    WellOperationCategory.IdRotor => IdSubsystemAPDRotor,
                    WellOperationCategory.IdSlide => IdSubsystemAPDSlide,
                    _ => throw new ArgumentException($"IdCategory: {group.Key} does not supported in this method",
                        nameof(group.Key))
                };

                var (sumDepthInterval, usedTimeHours, operationCount) = AggregateOperations(idSubsystem, group);

                var subsystemStat = new SubsystemStatDto
                {
                    IdSubsystem = idSubsystem,
                    SubsystemName = subsystems.TryGetValue(idSubsystem, out var subsystemDto)
                        ? subsystemDto.Name
                        : "unknown",
                    UsedTimeHours = usedTimeHours,
                    SumOperationDepthInterval = group.Sum(o => o.DepthEnd - o.DepthStart),
                    SumOperationDurationHours = group.Sum(o => o.DurationMinutes / 60),
                    SumDepthInterval = sumDepthInterval,
                    OperationCount = operationCount,
                };

                subsystemStat.KUsage = subsystemStat.SumDepthInterval / subsystemStat.SumOperationDepthInterval;

                return subsystemStat;
            });

        if (!apdRotorAndSlide.Any())
            return Enumerable.Empty<SubsystemStatDto>();

        var apdSum = new SubsystemStatDto
        {
            IdSubsystem = IdSubsystemAPD,
            SubsystemName =
                subsystems.TryGetValue(IdSubsystemAPD, out var subsystemDto) ? subsystemDto.Name : "unknown",
            UsedTimeHours = apdRotorAndSlide.Sum(part => part.UsedTimeHours),
            SumOperationDepthInterval = apdRotorAndSlide.Sum(part => part.SumOperationDepthInterval),
            SumOperationDurationHours = apdRotorAndSlide.Sum(part => part.SumOperationDurationHours),
            SumDepthInterval = apdRotorAndSlide.Sum(part => part.SumDepthInterval),
            OperationCount = apdRotorAndSlide.Sum(part => part.OperationCount),
        };

        apdSum.KUsage = apdSum.SumDepthInterval / apdSum.SumOperationDepthInterval;

        var apdStat = new List<SubsystemStatDto> { apdSum };
        apdStat.AddRange(apdRotorAndSlide);

        return apdStat;
    }

    private static (double SumDepthInterval, double UsedTimeHours, int Count) AggregateOperations(int idSubsystem,
        IEnumerable<DetectedOperationWithDrillerDto> operations) =>
        idSubsystem switch
        {
            IdSubsystemAPDRotor => CalcOperationsByEnableSubsystems(operations, EnabledSubsystemsFlags.AutoRotor),
            IdSubsystemAPDSlide => CalcOperationsByEnableSubsystems(operations,
                EnabledSubsystemsFlags.AutoSlide | EnabledSubsystemsFlags.AutoOscillation),
            IdSubsystemOscillation => CalcOperationsByEnableSubsystems(operations,
                EnabledSubsystemsFlags.AutoOscillation),
            _ => throw new ArgumentException($"IdSubsystem: {idSubsystem} does not supported in this method",
                nameof(idSubsystem))
        };

    private static (double SumDepthInterval, double UsedTimeHours, int OperationCount) CalcOperationsByEnableSubsystems(
        IEnumerable<DetectedOperationWithDrillerDto> operations,
        EnabledSubsystemsFlags enabledSubsystems)
    {
        var filtered = operations.Where(o => enabledSubsystems.HasEnabledSubsystems(o.EnabledSubsystems));

        var sumDepthInterval = filtered.Sum(o => o.DepthEnd - o.DepthStart);
        var usedTimeHours = filtered.Sum(o => o.DurationMinutes / 60);
        var operationCount = filtered.Count();

        return (sumDepthInterval, usedTimeHours, operationCount);
    }

    private async Task<IEnumerable<SubsystemActiveWellStatDto>> GetStatAsync(IEnumerable<WellDto> wells,
        DateTime? geDate,
        DateTime? leDate,
        CancellationToken token)
    {
        if (!wells.Any())
            return Enumerable.Empty<SubsystemActiveWellStatDto>();

        var idsTelemetries = wells
            .Where(w => w.IdTelemetry is not null)
            .Select(w => w.IdTelemetry!.Value)
            .Distinct();

        var wellsStat = new List<SubsystemActiveWellStatDto>();

        foreach (var well in wells)
        {
            var hoursOffset = well.Timezone.Hours;

            var geDateStartUtc = geDate?.ToUtcDateTimeOffset(hoursOffset);

            var leDateUtc = leDate?.ToUtcDateTimeOffset(hoursOffset);

            var request = new DetectedOperationByWellRequest
            {
                IdWell = well.Id,
                IdsCategories = WellOperationCategory.MechanicalDrillingSubIds,
                GeDateStart = geDateStartUtc,
                LeDateEnd = leDateUtc,
            };

            var telemetryOperations = await detectedOperationService
                .GetOperationsAsync(request, token);

            var wellStat = new SubsystemActiveWellStatDto { Well = well };

            if (!telemetryOperations.Any())
                continue;

            var subsystemStat = await CalcStatAsync(telemetryOperations, token);

            if (!subsystemStat.Any())
                continue;

            wellStat.SubsystemAPD = subsystemStat.FirstOrDefault(s => s.IdSubsystem == IdSubsystemAPD);
            wellStat.SubsystemOscillation = subsystemStat.FirstOrDefault(s => s.IdSubsystem == IdSubsystemOscillation);
            wellsStat.Add(wellStat);
        }

        return wellsStat;
    }
}