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.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;

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

	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 ltDate = new DateTimeOffset(dailyReport.Date.AddDays(1), TimeOnly.MinValue, TimeSpan.FromHours(offsetHours));

        var factOperationRequest = new WellOperationRequest
		{
			IdWell = idWell,
			OperationType = WellOperation.IdOperationTypeFact,
			GeDate = geDate,
			LeDate = ltDate
		};

		var factWellOperations = (await wellOperationRepository.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, cancellationToken);
		await UpdateSubsystemBlockAsync(dailyReport, cancellationToken);
		
		await AddTrajectoryBlockAsync(dailyReport, cancellationToken);
		await AddScheduleBlockAsync(dailyReport, cancellationToken);
		await AddProcessMapWellDrillingBlockAsync(dailyReport, 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 GetDatesRangeAsync(idWell, 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;
		}

		if (datesRange.From.AddDays(result.Skip) <= datesRange.To)
			result.Count = (int)(Math.Ceiling((datesRange.To - DateTimeOffset.UnixEpoch).TotalDays) -
								Math.Floor((datesRange.From - DateTimeOffset.UnixEpoch).TotalDays)) + 1;

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

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

		var factWellOperations = await wellOperationRepository.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 = date.ToDateTime(TimeOnly.MinValue, DateTimeKind.Unspecified);
			var leDate = date.AddDays(1).ToDateTime(TimeOnly.MinValue, DateTimeKind.Unspecified);

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

	public async Task<DatesRangeDto?> GetDatesRangeAsync(int idWell, CancellationToken cancellationToken)
	{
		var timezone = wellService.GetTimezone(idWell);
		var currentDate = DateTimeOffset.UtcNow.ToOffset(TimeSpan.FromHours(timezone.Hours)); 
		
		var factOperationDatesRange = await wellOperationRepository.GetDatesRangeAsync(idWell,  WellOperation.IdOperationTypeFact, 
			cancellationToken);

		if (factOperationDatesRange is null)
			return null;

		var from = (factOperationDatesRange.From.AddDays(1) <= DateTimeOffset.UtcNow.ToOffset(TimeSpan.FromHours(timezone.Hours)) ? 
				factOperationDatesRange.From : 
				currentDate.AddDays(-1));

		var to = (factOperationDatesRange.To.AddDays(1) <= DateTimeOffset.UtcNow.ToOffset(TimeSpan.FromHours(timezone.Hours)) ? 
				factOperationDatesRange.To : 
				currentDate.AddDays(-1));

		return new DatesRangeDto
		{
			From = from,
			To = to
		};
	}

	private async Task UpdateTimeBalanceBlockAsync(DailyReportDto dailyReport, IEnumerable<WellOperationDto> factWellOperations,
		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;

			var geDateStart = dailyReport.Date.ToDateTime(TimeOnly.MinValue, DateTimeKind.Unspecified);
			var leDateEnd = dailyReport.Date.AddDays(1).ToDateTime(TimeOnly.MinValue, DateTimeKind.Unspecified);
			
			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, CancellationToken cancellationToken)
	{
		var geDate = dailyReport.Date.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc);
		var leDate = dailyReport.Date.AddDays(1).ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc);
		
		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, CancellationToken cancellationToken)
	{
		var workDate = dailyReport.Date.ToDateTime(TimeOnly.MinValue, DateTimeKind.Unspecified);
		
		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, 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 geDate = dailyReport.Date.ToDateTime(TimeOnly.MinValue, DateTimeKind.Unspecified);
			var leDate = dailyReport.Date.AddDays(1).ToDateTime(TimeOnly.MinValue, DateTimeKind.Unspecified);
			
			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, CancellationToken cancellationToken)
	{
		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 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().CategoryName,
					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 GetDatesRangeAsync(idWell, cancellationToken);

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

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