using AsbCloudApp.Data.DailyReport;
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;
using AsbCloudApp.Data;
using AsbCloudApp.Data.DailyReport.Blocks;
using AsbCloudApp.Data.DailyReport.Blocks.Sign;
using AsbCloudApp.Data.DailyReport.Blocks.Subsystems;
using AsbCloudApp.Data.DailyReport.Blocks.TimeBalance;
using AsbCloudApp.Data.DailyReport.Blocks.WellOperation;
using AsbCloudApp.Data.WellOperation;
using AsbCloudApp.Requests;
using AsbCloudApp.Services.DailyReport;
using AsbCloudApp.Services.ProcessMaps.WellDrilling;
using AsbCloudDb.Model;
using Mapster;

namespace AsbCloudInfrastructure.Services.DailyReport;

public class DailyReportService : IDailyReportService
{
   private readonly IWellService wellService;
   private readonly ITrajectoryNnbRepository trajectoryFactNnbRepository;
   private readonly IDailyReportRepository dailyReportRepository;
   private readonly IScheduleRepository scheduleRepository;
   private readonly IWellOperationRepository wellOperationRepository;
   private readonly ISubsystemService subsystemService;
   private readonly IProcessMapReportDrillingService processMapReportDrillingService;
   private readonly IDetectedOperationService detectedOperationService;
   private readonly IWellOperationService wellOperationService;

    public DailyReportService(IWellService wellService,
        ITrajectoryNnbRepository trajectoryFactNnbRepository,
        IDailyReportRepository dailyReportRepository,
        IScheduleRepository scheduleRepository,
        IWellOperationRepository wellOperationRepository,
        ISubsystemService subsystemService,
        IProcessMapReportDrillingService processMapReportDrillingService,
        IDetectedOperationService detectedOperationService,
        IWellOperationService wellOperationService)
    {
        this.wellService = wellService;
        this.trajectoryFactNnbRepository = trajectoryFactNnbRepository;
        this.dailyReportRepository = dailyReportRepository;
        this.scheduleRepository = scheduleRepository;
        this.wellOperationRepository = wellOperationRepository;
        this.subsystemService = subsystemService;
        this.processMapReportDrillingService = processMapReportDrillingService;
        this.detectedOperationService = detectedOperationService;
        this.wellOperationService = wellOperationService;
    }

    public async Task<int> UpdateOrInsertAsync<TBlock>(int idWell, DateOnly dateDailyReport, int idUser, TBlock editableBlock,
      CancellationToken cancellationToken)
      where TBlock : ItemInfoDto
   {
      if (!await IsDateDailyReportInRangeAsync(idWell, dateDailyReport, cancellationToken))
         throw new ArgumentInvalidException(nameof(dateDailyReport), "Невозможно обновить суточный отчёт");
      
      var dailyReport = await dailyReportRepository.GetOrDefaultAsync(idWell, dateDailyReport, cancellationToken) ??
                  new DailyReportDto
                  {
                     IdWell = idWell,
                     Date = dateDailyReport
                  };

      switch (editableBlock)
      {
         case SubsystemBlockDto subsystemBlock:
            dailyReport.SubsystemBlock = subsystemBlock;
            break;
         case TimeBalanceBlockDto timeBalanceBlock:
            dailyReport.TimeBalanceBlock = timeBalanceBlock;
            break;
         case SignBlockDto signBlock:
            dailyReport.SignBlock = signBlock;
            break;
         default:
            throw new InvalidOperationException("Unexpected type of editableBlock");
      }

      editableBlock.IdUser = idUser;
      editableBlock.LastUpdateDate = DateTime.UtcNow;

      dailyReport.DateLastUpdate = DateTimeOffset.UtcNow;

      if (dailyReport.Id == 0)
         return await dailyReportRepository.InsertAsync(dailyReport, cancellationToken);
      
      return await dailyReportRepository.UpdateAsync(dailyReport, cancellationToken);
   }

   public async Task<DailyReportDto> GetAsync(int idWell, DateOnly dateDailyReport, CancellationToken cancellationToken)
   {
      var well = await wellService.GetOrDefaultAsync(idWell, cancellationToken)
         ?? throw new ArgumentNullException(nameof(idWell), $"Скважина с Id: {idWell} не найдена");

        if (!await IsDateDailyReportInRangeAsync(idWell, dateDailyReport, cancellationToken))
         throw new ArgumentInvalidException(nameof(dateDailyReport), "Невозможно получить суточный отчёт");
      

      var dailyReport = await dailyReportRepository.GetOrDefaultAsync(idWell, dateDailyReport, cancellationToken) ?? new DailyReportDto
      {
         Date = dateDailyReport,
         IdWell = well.Id
      };

        var offsetHours = wellService.GetTimezone(dailyReport.IdWell).Hours;
        var geDate = new DateTimeOffset(dailyReport.Date, TimeOnly.MinValue, TimeSpan.FromHours(offsetHours));
      var leDate = new DateTimeOffset(dailyReport.Date.AddDays(1), TimeOnly.MinValue, TimeSpan.FromHours(offsetHours));

      var factOperationRequest = new WellOperationRequest(new []{ idWell })
      { 
         OperationType = WellOperation.IdOperationTypeFact,
         GeDate = geDate,
         LeDate = leDate
      };

      var factWellOperations = (await wellOperationService.GetAsync(factOperationRequest, cancellationToken))
         .OrderBy(o => o.DateStart)
         .ThenBy(o => o.DepthStart);

      dailyReport.WellCaption = well.Caption;
      dailyReport.WellType = well.WellType;
      dailyReport.Cluster = well.Cluster;
      dailyReport.Deposit = well.Deposit;
      dailyReport.Customer = well.Companies.FirstOrDefault(c => c.IdCompanyType == 1)?.Caption;
      dailyReport.Contractor = well.Companies.FirstOrDefault(c => c.IdCompanyType == 2)?.Caption;
      dailyReport.DepthStart = factWellOperations.FirstOrDefault()?.DepthStart;
      dailyReport.DepthEnd = factWellOperations.LastOrDefault()?.DepthEnd;

      await UpdateTimeBalanceBlockAsync(dailyReport, factWellOperations, geDate, leDate, cancellationToken);
      await UpdateSubsystemBlockAsync(dailyReport, geDate, leDate, cancellationToken);
      
      await AddTrajectoryBlockAsync(dailyReport, geDate, leDate, cancellationToken);
      await AddScheduleBlockAsync(dailyReport, geDate, cancellationToken);
      await AddProcessMapWellDrillingBlockAsync(dailyReport, geDate, leDate, cancellationToken);

      AddFactWellOperationBlock(dailyReport, factWellOperations);

      return dailyReport;
   }

   public async Task<PaginationContainer<DailyReportDto>> GetAsync(int idWell, FileReportRequest request,
      CancellationToken cancellationToken)
   {
      var result = new PaginationContainer<DailyReportDto>
      {
         Skip = request.Skip ?? 0,
         Take = request.Take ?? 10,
         Items = Enumerable.Empty<DailyReportDto>()
      };

      var datesRange = await wellOperationRepository.GetDatesRangeAsync(idWell, WellOperation.IdOperationTypeFact, cancellationToken);

      if (datesRange is null)
         return result;

      var dailyReports = new List<DailyReportDto>();
      TimeSpan offset = wellService.GetTimezone(idWell).Offset;

        if (request.GeDate.HasValue)
      {
         var startDate = new DateTimeOffset(request.GeDate.Value, TimeOnly.MinValue, offset);

         if (startDate >= datesRange.From)
            datesRange.From = startDate;
      }

      if (request.LeDate.HasValue)
      {
         var finishDate = new DateTimeOffset(request.LeDate.Value, TimeOnly.MinValue, offset);

         if (finishDate <= datesRange.To)
            datesRange.To = finishDate;
      }

      result.Count = (datesRange.To.Day - DateTimeOffset.UnixEpoch.Day) - (datesRange.From.Day - DateTimeOffset.UnixEpoch.Day);

      var existingDailyReports = await dailyReportRepository.GetAsync(idWell, request, cancellationToken);

      var geDateFactWellOperation = datesRange.From.AddDays(result.Skip);
      var leDateFactWellOperation = geDateFactWellOperation.AddDays(result.Take);
      
      var factWellOperationRequest = new WellOperationRequest(new[] { idWell })
      {
         OperationType = WellOperation.IdOperationTypeFact,
         GeDate = geDateFactWellOperation,
         LeDate = leDateFactWellOperation
      };

      var factWellOperations = await wellOperationService.GetAsync(factWellOperationRequest, cancellationToken);

      if (request.SortFields?.Contains("DateStart desc") == true)
      {
         for (var day = result.Skip; day - result.Skip < result.Take && datesRange.To.AddDays(-day) >= datesRange.From; day++)
         {
            var dateDailyReport = DateOnly.FromDateTime(datesRange.To.AddDays(-day).DateTime);

            AddDailyReport(dateDailyReport);
         }
      }
      else
      {
         for (var day = result.Skip; day - result.Skip < result.Take && datesRange.From.AddDays(day) <= datesRange.To; day++)
         {
            var dateDailyReport = DateOnly.FromDateTime(datesRange.From.AddDays(day).DateTime);

            AddDailyReport(dateDailyReport);
         }
      }

      result.Items = dailyReports;

      return result;

      void AddDailyReport(DateOnly date)
      {
         var dailyReport = existingDailyReports.FirstOrDefault(d => d.IdWell == idWell && d.Date == date)
            ?? new DailyReportDto
            {
               Date = date,
               IdWell = idWell
            };

            var geDate = new DateTimeOffset(date, TimeOnly.MinValue, offset);
            var leDate = new DateTimeOffset(date.AddDays(1), TimeOnly.MinValue, offset);

            var factWellOperationPerDay = factWellOperations.Where(o => o.DateStart >= geDate && 
                                                      o.DateStart <= leDate);
         
         AddFactWellOperationBlock(dailyReport, factWellOperationPerDay);
         
         dailyReports.Add(dailyReport);
      }
   }

   private async Task UpdateTimeBalanceBlockAsync(DailyReportDto dailyReport, IEnumerable<WellOperationDto> factWellOperations,
      DateTimeOffset geDateStart, DateTimeOffset leDateEnd, CancellationToken cancellationToken)
   {
      const int idWellOperationSlipsTime = 5011;

      if (dailyReport.TimeBalanceBlock is not null)
      {
         dailyReport.TimeBalanceBlock.SectionName = wellOperationRepository.GetSectionTypes()
            .FirstOrDefault(s => s.Id == dailyReport.TimeBalanceBlock.IdSection)?.Caption;

            dailyReport.TimeBalanceBlock.WellOperationSlipsTimeCount = (await detectedOperationService.GetAsync(
            new DetectedOperationByWellRequest
            {
               IdsCategories = new[] { idWellOperationSlipsTime },
               IdWell = dailyReport.IdWell,
               GeDateStart = geDateStart,
               LeDateEnd = leDateEnd
            }, cancellationToken))?.Stats.Sum(s => s.Count);

         dailyReport.TimeBalanceBlock.WellDepth.Fact = factWellOperations
            .Where(o => o.IdWellSectionType == dailyReport.TimeBalanceBlock.IdSection)
            .Sum(o => o.DepthEnd - o.DepthStart);
      }
   }

   private async Task AddTrajectoryBlockAsync(DailyReportDto dailyReport, 
      DateTimeOffset geDate, DateTimeOffset leDate, CancellationToken cancellationToken)
   {
      var trajectory = (await trajectoryFactNnbRepository.GetByRequestAsync(new TrajectoryRequest
      {
         IdWell = dailyReport.IdWell,
         GeDate = geDate,
         LeDate = leDate
      }, cancellationToken)).MaxBy(t => t.WellboreDepth);

      dailyReport.TrajectoryBlock = new TrajectoryBlockDto
      {
         WellboreDepth = trajectory?.WellboreDepth,
         VerticalDepth = trajectory?.VerticalDepth,
         ZenithAngle = trajectory?.ZenithAngle,
         AzimuthGeo = trajectory?.AzimuthGeo
      };
   }

   private async Task AddScheduleBlockAsync(DailyReportDto dailyReport, DateTimeOffset workDate, CancellationToken cancellationToken)
   {
        dailyReport.ScheduleBlock = (await scheduleRepository.GetAsync(dailyReport.IdWell, workDate, cancellationToken))
         .Select(s => new ScheduleRecordDto
         {
            ShiftStart = s.ShiftStart,
            ShiftEnd = s.ShiftEnd,
            Name = s.Driller?.Name,
            Surname = s.Driller?.Surname,
            Patronymic = s.Driller?.Patronymic
         });
   }

   private async Task UpdateSubsystemBlockAsync(DailyReportDto dailyReport, 
      DateTimeOffset geDate, DateTimeOffset leDate, CancellationToken cancellationToken)
   {
      dailyReport.SubsystemBlock ??= new SubsystemBlockDto();

      dailyReport.SubsystemBlock.Subsystems = await GetSubsystemsAsync();

      async Task<IEnumerable<SubsystemRecordDto>> GetSubsystemsAsync()
      {
         var subsystemsStatPerWell = await subsystemService.GetStatAsync(new SubsystemRequest
         {
            IdWell = dailyReport.IdWell
         }, cancellationToken);
         
         var subsystemsStatPerDay = await subsystemService.GetStatAsync(new SubsystemRequest
         {
            IdWell = dailyReport.IdWell,
            GeDate = geDate,
            LeDate = leDate
         }, cancellationToken);

         var subsystems = subsystemsStatPerWell
            .Select(subsystemStatPerWell => new SubsystemRecordDto
            {
               Name = subsystemStatPerWell.SubsystemName, 
               UsagePerDay = subsystemsStatPerDay.FirstOrDefault(s => s.IdSubsystem == subsystemStatPerWell.IdSubsystem)?.Adapt<SubsystemParametersDto>(), 
               UsagePerWell = subsystemStatPerWell.Adapt<SubsystemParametersDto>()
            }).ToList();

         if (dailyReport.SubsystemBlock?.Subsystems != null && dailyReport.SubsystemBlock.Subsystems.Any())
            subsystems.AddRange(dailyReport.SubsystemBlock.Subsystems);

         return subsystems.OrderBy(s => s.Name);
      }
   }

   private async Task AddProcessMapWellDrillingBlockAsync(DailyReportDto dailyReport, 
      DateTimeOffset geDate, DateTimeOffset leDate, CancellationToken cancellationToken)
   {
      var request = new DataSaubStatRequest();
        dailyReport.ProcessMapWellDrillingBlock = (await processMapReportDrillingService.GetAsync(dailyReport.IdWell, request, 
            cancellationToken)).Where(p => p.DateStart >= geDate && p.DateStart <= leDate)
         .GroupBy(p => p.DrillingMode)
         .Select(g => new ProcessMapWellDrillingRecordDto
         {
            DrillingMode = g.Key,
            WellBoreDepth = g.Sum(p => p.DeltaDepth),
            Rop = new PlanFactDto<double?>
            {
               Plan = g.Sum(p => p.Rop.Plan),
               Fact = g.Sum(p => p.Rop.Fact)
            },
            MechDrillingHours = g.Sum(p => p.DrilledTime)
         });
   }

   private static void AddFactWellOperationBlock(DailyReportDto dailyReport, IEnumerable<WellOperationDto> factWellOperations)
   {
      const int idWellOperationCategoryDrilling = 4001;

      dailyReport.FactWellOperationBlock = new WellOperationBlockDto
      {
         WellOperations = factWellOperations.GroupBy(o => o.IdCategory)
            .Select(g => new WellOperationRecordDto
            {
               CategoryName = g.First().OperationCategoryName,
               DurationHours = g.Sum(o => o.DurationHours)
            }),

         SectionDrillingHours = factWellOperations
            .Where(o => o.IdParentCategory is idWellOperationCategoryDrilling)
            .Sum(o => o.DurationHours)
      };
   }

   private async Task<bool> IsDateDailyReportInRangeAsync(int idWell, DateOnly dateDailyReport, CancellationToken cancellationToken)
   {
      var datesRange = await wellOperationRepository.GetDatesRangeAsync(idWell, WellOperation.IdOperationTypeFact, cancellationToken);

      if (datesRange is null)
         return false;
      var from = DateOnly.FromDateTime(datesRange.From.DateTime);
      var to = DateOnly.FromDateTime(datesRange.To.DateTime);

      return dateDailyReport >= from && dateDailyReport <= to;
   }
}