using AsbCloudApp.Data;
using AsbCloudApp.Services;
using AsbCloudDb.Model;
using Mapster;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AsbCloudApp.Data.SAUB;
using AsbCloudApp.Data.WellOperation;
using AsbCloudApp.Repositories;

namespace AsbCloudInfrastructure.Services.WellOperationService;

public class OperationsStatService : IOperationsStatService
{
    private readonly IAsbCloudDbContext db;
    private readonly IMemoryCache memoryCache;
    private readonly IWellService wellService;
	private readonly ITelemetryDataCache<TelemetryDataSaubDto> telemetryDataCache;

	public OperationsStatService(IAsbCloudDbContext db, IMemoryCache memoryCache, IWellService wellService, 
		ITelemetryDataCache<TelemetryDataSaubDto> telemetryDataCache)
    {
        this.db = db;
        this.memoryCache = memoryCache;
        this.wellService = wellService;
			this.telemetryDataCache = telemetryDataCache;
		}

    public async Task<StatClusterDto?> GetOrDefaultStatClusterAsync(int idCluster, int idCompany, CancellationToken token)
    {
        var cluster = (await memoryCache
            .GetOrCreateBasicAsync(db.Set<Cluster>(), token))
            .FirstOrDefault(c => c.Id == idCluster);

        if (cluster is null)
            return null;

        var allWellsByCompany = await wellService.GetAsync(new() { IdCompany = idCompany }, token).ConfigureAwait(false);

        var idWellsByCompany = allWellsByCompany.Select(w => w.Id).Distinct();

        var wells = await db.Wells
            .Include(w => w.WellOperations)
            .Where(o => o.IdCluster == idCluster)
            .Where(w => idWellsByCompany.Contains(w.Id))
            .Select(w => w.Id)
            .ToListAsync(token);

        var statsWells = await GetWellsStatAsync(wells, token).ConfigureAwait(false);

        var statClusterDto = new StatClusterDto
        {
            Id = idCluster,
            Caption = cluster.Caption,
            StatsWells = statsWells,
        };
        return statClusterDto;
    }

    public async Task<IEnumerable<StatWellDto>> GetWellsStatAsync(IEnumerable<int> idWells, CancellationToken token)
    {
        var wells = await db.Wells
            .Include(w => w.WellOperations)
            .Where(w => idWells.Contains(w.Id))
            .AsNoTracking()
            .ToListAsync(token);

        var statsWells = new List<StatWellDto>(wells.Count);

        foreach (var well in wells)
        {
            var statWellDto = await CalcWellStatAsync(well, token);
            statsWells.Add(statWellDto);
        }
        return statsWells;
    }

    public async Task<StatWellDto?> GetOrDefaultWellStatAsync(int idWell,
        CancellationToken token = default)
    {
        var well = await db.Wells
            .Include(w => w.WellOperations)
            .FirstOrDefaultAsync(w => w.Id == idWell, token)
            .ConfigureAwait(false);

        if(well is null)
            return null;

        var statWellDto = await CalcWellStatAsync(well, token);
        return statWellDto;
    }

    public async Task<ClusterRopStatDto?> GetOrDefaultRopStatAsync(int idWell, CancellationToken token)
    {
        var clusterWellsIds = await wellService.GetClusterWellsIdsAsync(idWell, token)
            .ConfigureAwait(false);

        if (clusterWellsIds is null)
            return null;

        var idLastSectionType = await (from o in db.WellOperations
                                       where o.IdWell == idWell &&
                                             o.IdType == 1
                                       orderby o.DepthStart
                                       select o.IdWellSectionType)
            .LastOrDefaultAsync(token)
            .ConfigureAwait(false);

        if (idLastSectionType == default)
            return null;

        var operations = await (from o in db.WellOperations
                                where clusterWellsIds.Contains(o.IdWell) &&
                                      o.IdType == 1 &&
                                      o.IdWellSectionType == idLastSectionType
                                select o)
            .ToListAsync(token)
            .ConfigureAwait(false);

        var statsList = new List<StatOperationsDto>(clusterWellsIds.Count());
        foreach (var clusterWellId in clusterWellsIds)
        {
            var currentWellOps = operations.Where(o => o.IdWell == clusterWellId);
            var timezoneOffsetHours = wellService.GetTimezone(clusterWellId).Hours;
            var stat = CalcStat(currentWellOps, timezoneOffsetHours);
            if(stat is not null)
                statsList.Add(stat);
        };

        if (!statsList.Any())
            return null;

        var clusterRops = new ClusterRopStatDto()
        {
            RopMax = statsList.Max(s => s.Rop),
            RopAverage = statsList.Average(s => s.Rop)
        };

        return clusterRops;
    }

    private async Task<StatWellDto> CalcWellStatAsync(Well well, CancellationToken token)
    {
        var timezone = wellService.GetTimezone(well.Id);

        var wellType = (await memoryCache
            .GetOrCreateBasicAsync(db.Set<WellType>(), token))
            .FirstOrDefault(t => t.Id == well.IdWellType);
        var statWellDto = new StatWellDto
        {
            Id = well.Id,
            Caption = well.Caption,
            WellType = wellType?.Caption ?? "",
            IdState = well.IdState,
            State = wellService.GetStateText(well.IdState),
            LastTelemetryDate = wellService.GetLastTelemetryDate(well.Id),
            Companies = await wellService.GetCompaniesAsync(well.Id, token)
        };

        if (well.WellOperations is null)
            return statWellDto;

        var wellOperations = well.WellOperations
            .OrderBy(o => o.DateStart)
            .ThenBy(o => o.DepthEnd);

        if (!wellOperations.Any())
            return statWellDto;

        statWellDto.Sections = CalcSectionsStats(wellOperations, timezone.Hours);
        statWellDto.Total = GetStatTotal(wellOperations, well.IdState, timezone.Hours);
        statWellDto.TvdLagDays = CalcTvdLagDays(wellOperations);
        statWellDto.TvdDrillingDays = CalcDrillingDays(wellOperations);

        return statWellDto;
    }

    private static double? CalcDrillingDays(IEnumerable<WellOperation> wellOperations)
    {
        var operationsOrdered = wellOperations
            .OrderBy(o => o.DateStart);
        
        var factOperations = operationsOrdered
            .Where(o => o.IdType == WellOperation.IdOperationTypeFact);

        if (!factOperations.Any())
            return null;
        
        var operationFrom = factOperations.First();

        var operationTo = factOperations.Last();

        return (operationTo.DateStart.AddHours(operationTo.DurationHours) - operationFrom.DateStart).TotalDays;
    }

    private static double? CalcTvdLagDays(IEnumerable<WellOperation> wellOperations)
    {
        var operationsOrdered = wellOperations
            .OrderBy(o => o.DateStart);

        var factOperations = operationsOrdered
            .Where(o => o.IdType == WellOperation.IdOperationTypeFact);

        var lastCorrespondingFactOperation = factOperations
            .LastOrDefault(o => o.IdPlan is not null);
            
        if (lastCorrespondingFactOperation is null)
            return null;
            
        var lastCorrespondingPlanOperation = wellOperations
            .FirstOrDefault(o => o.Id == lastCorrespondingFactOperation.IdPlan);

        if (lastCorrespondingPlanOperation is null) 
            return null;

        var factEnd = lastCorrespondingFactOperation.DateStart.AddHours(lastCorrespondingFactOperation.DurationHours);
        var planEnd = lastCorrespondingPlanOperation.DateStart.AddHours(lastCorrespondingPlanOperation.DurationHours);
        var lagDays = (factEnd - planEnd).TotalDays;

        return lagDays;
    }

    private IEnumerable<StatSectionDto> CalcSectionsStats(IEnumerable<WellOperation> operations, double timezoneOffsetH)
    {
        var sectionTypeIds = operations
            .Select(o => o.IdWellSectionType)
            .Distinct();

        var sectionTypes = memoryCache
            .GetOrCreateBasic(db.Set<WellSectionType>())
            .Where(s => sectionTypeIds.Contains(s.Id))
            .ToDictionary(s => s.Id);

        var sections = new List<StatSectionDto>(sectionTypes.Count);
        var operationsPlan = operations.Where(o => o.IdType == WellOperation.IdOperationTypePlan);
        var operationsFact = operations.Where(o => o.IdType == WellOperation.IdOperationTypeFact);

        foreach ((var id, var sectionType) in sectionTypes)
        {
            var section = new StatSectionDto
            {
                Id = id,
                Caption = sectionType.Caption,
                Plan = CalcSectionStat(operationsPlan, id, timezoneOffsetH),
                Fact = CalcSectionStat(operationsFact, id, timezoneOffsetH),
            };
            sections.Add(section);
        }
        return sections;
    }

    private static PlanFactDto<StatOperationsDto> GetStatTotal(IEnumerable<WellOperation> operations,
        int idWellState, double timezoneOffsetH)
    {
        var operationsPlan = operations.Where(o => o.IdType == WellOperation.IdOperationTypePlan);
        var operationsFact = operations.Where(o => o.IdType == WellOperation.IdOperationTypeFact);
        var factEnd = CalcStat(operationsFact, timezoneOffsetH);
        if (factEnd is not null && idWellState != 2)
            factEnd.End = null;
        var section = new PlanFactDto<StatOperationsDto>
        {
            Plan = CalcStat(operationsPlan, timezoneOffsetH),
            Fact = factEnd,
        };
        return section;
    }

    private static StatOperationsDto? CalcSectionStat(IEnumerable<WellOperation> operations, int idSectionType, double timezoneOffsetHours)
    {
        var sectionOperations = operations
            .Where(o => o.IdWellSectionType == idSectionType)
            .OrderBy(o => o.DateStart)
            .ThenBy(o => o.DepthStart);

        return CalcStat(sectionOperations, timezoneOffsetHours);
    }

    private static StatOperationsDto? CalcStat(IEnumerable<WellOperation> operations, double timezoneOffsetHours)
    {
        if (!operations.Any())
            return null;

        var races = GetCompleteRaces(operations, timezoneOffsetHours);

        var section = new StatOperationsDto
        {
            Start = operations.FirstOrDefault()?.DateStart.ToOffset(TimeSpan.FromHours(timezoneOffsetHours)),
            End = operations.Max(o => o.DateStart.ToOffset(TimeSpan.FromHours(timezoneOffsetHours)).AddHours(o.DurationHours)),
            WellDepthStart = operations.Min(o => o.DepthStart),
            WellDepthEnd = operations.Max(o => o.DepthStart),
            Rop = CalcROP(operations),
            RouteSpeed = CalcAvgRaceSpeed(races),
            BhaDownSpeed = CalcBhaDownSpeed(races),
            BhaUpSpeed = CalcBhaUpSpeed(races),
            CasingDownSpeed = CalcCasingDownSpeed(operations),
            NonProductiveHours = operations
                .Where(o => WellOperationCategory.NonProductiveTimeSubIds.Contains(o.IdCategory))
                .Sum(o => o.DurationHours),
        };
        return section;
    }

    private static double CalcROP(IEnumerable<WellOperation> operationsProps)
    {
        var drillingOperations = operationsProps.Where(o => WellOperationCategory.MechanicalDrillingSubIds.Contains(o.IdCategory));
        var dDepth = 0d;
        var dHours = 0d;
        foreach (var operation in drillingOperations)
        {
            var deltaDepth = operation.DepthEnd - operation.DepthStart;
            dDepth += deltaDepth;
            dHours += operation.DurationHours;
        }
        return dDepth / (dHours + double.Epsilon);
    }

    private static double CalcCasingDownSpeed(IEnumerable<WellOperation> operationsProps)
    {
        var ops = operationsProps.Where(o => o.IdCategory == WellOperationCategory.IdCasingDown);
        var depth = 0d;
        var dHours = 0d;
        foreach (var operation in ops)
        {
            depth += operation.DepthStart;
            dHours += operation.DurationHours;
        }
        return depth / (dHours + double.Epsilon);
    }

    private static IEnumerable<Race> GetCompleteRaces(IEnumerable<WellOperation> operations, double timezoneOffsetH)
    {
        var races = new List<Race>();
        var iterator = operations
            .OrderBy(o => o.DateStart)
            .GetEnumerator();
        while (iterator.MoveNext())
        {
            if (iterator.Current.IdCategory == WellOperationCategory.IdBhaAssembly)
            {
                var race = new Race
                {
                    StartDate = iterator.Current.DateStart.ToRemoteDateTime(timezoneOffsetH).AddHours(iterator.Current.DurationHours),
                    StartWellDepth = iterator.Current.DepthStart,
                    Operations = new List<WellOperation>(10),
                };
                while (iterator.MoveNext())
                {
                    if (iterator.Current.IdCategory == WellOperationCategory.IdEquipmentRepair)
                        race.RepairHours += iterator.Current.DurationHours;

                    if (WellOperationCategory.NonProductiveTimeSubIds.Contains(iterator.Current.IdCategory))
                        race.NonProductiveHours += iterator.Current.DurationHours;

                    if (iterator.Current.IdCategory == WellOperationCategory.IdBhaDisassembly)
                    {
                        race.EndDate = iterator.Current.DateStart.ToRemoteDateTime(timezoneOffsetH);
                        race.EndWellDepth = iterator.Current.DepthStart;
                        races.Add(race);
                        break;
                    }
                    race.Operations.Add(iterator.Current);
                }
            }
        }
        return races;
    }

    private static double CalcAvgRaceSpeed(IEnumerable<Race> races)
    {
        var dDepth = 0d;
        var dHours = 0d;
        foreach (var race in races)
        {
            dHours += race.DeltaHours - race.NonProductiveHours - race.RepairHours;
            dDepth += race.DeltaDepth;
        }
        return dDepth / (dHours + double.Epsilon);
    }

    private static double CalcBhaDownSpeed(IEnumerable<Race> races)
    {
        var dDepth = 0d;
        var dHours = 0d;
        foreach (Race race in races)
        {
            dDepth += race.StartWellDepth;
            for (var i = 0; i < race.Operations.Count; i++)
            {
                if (race.Operations[i].IdCategory == WellOperationCategory.IdBhaDown)
                    dHours += race.Operations[i].DurationHours;
                if (WellOperationCategory.MechanicalDrillingSubIds.Contains(race.Operations[i].IdCategory))
                    break;
            }
        }
        return dDepth / (dHours + double.Epsilon);
    }

    private static double CalcBhaUpSpeed(IEnumerable<Race> races)
    {
        var dDepth = 0d;
        var dHours = 0d;
        foreach (var race in races)
        {
            dDepth += race.EndWellDepth;
            for (var i = race.Operations.Count - 1; i > 0; i--)
            {
                if (race.Operations[i].IdCategory == WellOperationCategory.IdBhaUp)
                    dHours += race.Operations[i].DurationHours;
                if (WellOperationCategory.MechanicalDrillingSubIds.Contains(race.Operations[i].IdCategory))
                    break;
            }
        }
        return dDepth / (dHours + double.Epsilon);
    }

    public async Task<IEnumerable<PlanFactPredictBase<WellOperationDto>>> GetTvdAsync(int idWell, CancellationToken token)
    {
        var wellOperations = (await GetOperationsAsync(idWell, token)).ToArray();
        if (!wellOperations.Any())
            return Enumerable.Empty<PlanFactPredictBase<WellOperationDto>>();
        
        var tzOffsetHours = wellService.GetTimezone(idWell).Hours;
        var tvd = new List<PlanFactPredictBase<WellOperationDto>>(wellOperations.Length);
        var (Plan, Fact) = wellOperations.FirstOrDefault();
        var dateStart = Plan?.DateStart ?? Fact!.DateStart;
        int? iLastMatch = null;
        int iLastFact = 0;
        var nptHours = 0d;
        for (int i = 0; i < wellOperations.Length; i++)
        {
            var item = wellOperations[i];
            var plan = item.Plan;
            var fact = item.Fact;

            var planFactPredict = new PlanFactPredictBase<WellOperationDto>();
            if (plan is not null)
            {
                planFactPredict.Plan = Convert(plan, tzOffsetHours);
                planFactPredict.Plan.Day = (planFactPredict.Plan.DateStart - dateStart).TotalDays;
                if (fact is not null)
                    iLastMatch = i;
            }

            if (fact is not null)
            {
                if(WellOperationCategory.NonProductiveTimeSubIds.Contains(fact.IdCategory))
                    nptHours += fact.DurationHours;

                planFactPredict.Fact = Convert(fact, tzOffsetHours);
                planFactPredict.Fact.Day = (planFactPredict.Fact.DateStart - dateStart).TotalDays;
                planFactPredict.Fact.NptHours = nptHours;
                iLastFact = i;
            }

            tvd.Add(planFactPredict);  
        }

        if (iLastMatch is null || iLastMatch == wellOperations.Length - 1)
            return tvd;

        var lastMatchPlan = wellOperations[iLastMatch.Value].Plan!;
        var lastMatchPlanOperationEnd = lastMatchPlan.DateStart.AddHours(lastMatchPlan.DurationHours);
        var lastFact = wellOperations[iLastFact].Fact!;
        var lastFactDateEnd = lastFact.DateStart.AddHours(lastFact.DurationHours);
        var startOffset = lastFactDateEnd - lastMatchPlanOperationEnd;

        for (int i = iLastMatch.Value + 1; i < wellOperations.Length; i++)
        {
            if (wellOperations[i].Plan is null)
                continue;
            var predict = Convert(wellOperations[i].Plan!, tzOffsetHours);
            predict.IdType = 2;
            predict.DateStart = predict.DateStart + startOffset;
            predict.Day = (predict.DateStart - dateStart).TotalDays;
            tvd[i].Predict = predict;
        }

        return tvd;
    }

    private async Task<IEnumerable<(WellOperation? Plan, WellOperation? Fact)>> GetOperationsAsync(int idWell, CancellationToken token)
    {
        var query = db.WellOperations
            .Include(o => o.OperationCategory)
            .Include(o => o.WellSectionType)
            .Where(o => o.IdWell == idWell)
            .OrderBy(o => o.DateStart)
            .ThenBy(o => o.DepthEnd);

        var operationsFactWithNoPlan = await query.Where(o => o.IdPlan == null && o.IdType == WellOperation.IdOperationTypeFact)
            .AsNoTracking()
            .ToArrayAsync(token);

        var operationsFactWithPlan = await query.Where(o => o.IdPlan != null && o.IdType == WellOperation.IdOperationTypeFact)
            .Include(o => o.OperationPlan)
            .ThenInclude(o => o!.WellSectionType)
            .Include(o => o.OperationPlan)
            .ThenInclude(o => o!.OperationCategory)
            .AsNoTracking()
            .ToArrayAsync(token);

        var idsPlanWithFact = operationsFactWithPlan.Select(o => o.IdPlan).Distinct();
        var operationsPlanWithNoFact = await query
            .Where(o => o.IdType == WellOperation.IdOperationTypePlan && !idsPlanWithFact.Contains(o.IdPlan)).ToArrayAsync(token);

        var capacity = operationsFactWithNoPlan.Length + operationsFactWithPlan.Length + operationsPlanWithNoFact.Length;
        var result = new List<(WellOperation? Plan, WellOperation? Fact)>(capacity);

        foreach (var operation in operationsFactWithPlan)
            result.Add((operation.OperationPlan, operation));

        foreach (var operation in operationsFactWithNoPlan)
            result.Add((null, operation));

        foreach (var operation in operationsPlanWithNoFact)
            result.Add((operation, null));

        return result
            .OrderBy(x => x.Plan?.DateStart)
            .ThenBy(x => x.Fact?.DateStart);
    }

    private static WellOperationDto Convert(WellOperation source, double tzOffsetHours)
    {
        var destination = source.Adapt<WellOperationDto>();
        destination.OperationCategoryName = source.OperationCategory.Name;
        destination.WellSectionTypeCaption = source.WellSectionType.Caption;
        destination.DateStart = source.DateStart.ToRemoteDateTime(tzOffsetHours);
        return destination;
    }
}