using AsbCloudApp.Data;
using AsbCloudApp.Data.DetectedOperation;
using AsbCloudApp.Data.Subsystems;
using AsbCloudApp.Exceptions;
using AsbCloudApp.Requests;
using AsbCloudApp.Services;
using AsbCloudApp.Services.Subsystems;
using AsbCloudDb;
using AsbCloudDb.Model;
using AsbCloudDb.Model.Subsystems;
using Mapster;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace AsbCloudInfrastructure.Services.Subsystems;

internal class SubsystemOperationTimeService : ISubsystemOperationTimeService
{
    private readonly IAsbCloudDbContext db;
    private readonly IWellService wellService;
    private readonly ICrudRepository<SubsystemDto> subsystemService;
    private readonly IDetectedOperationService detectedOperationService;
    public const int IdSubsystemAKB = 1;
    public const int IdSubsystemAKBRotor = 11;
    public const int IdSubsystemAKBSlide = 12;
    public const int IdSubsystemMSE = 2;
    public const int IdSubsystemSpin = 65536;
    public const int IdSubsystemTorque = 65537;

    public SubsystemOperationTimeService(IAsbCloudDbContext db, IWellService wellService, ICrudRepository<SubsystemDto> subsystemService, IDetectedOperationService detectedOperationService)
    {
        this.db = db;
        this.wellService = wellService;
        this.subsystemService = subsystemService;
        this.detectedOperationService = detectedOperationService;
    }

    /// <inheritdoc/>
    public async Task<int> DeleteAsync(SubsystemOperationTimeRequest request, CancellationToken token)
    {
        var well = await wellService.GetOrDefaultAsync(request.IdWell, token)
            ?? throw new ArgumentInvalidException(nameof(request.IdWell), $"Well Id: {request.IdWell} does not exist");

        var query = BuildQuery(request, well);
        db.SubsystemOperationTimes.RemoveRange(query);
        return await db.SaveChangesAsync(token);
    }

    /// <inheritdoc/>
    public async Task<IEnumerable<SubsystemOperationTimeDto>> GetOperationTimeAsync(SubsystemOperationTimeRequest request, CancellationToken token)
    {
        var well = await wellService.GetOrDefaultAsync(request.IdWell, token)
            ?? throw new ArgumentInvalidException(nameof(request.IdWell), $"Well Id: {request.IdWell} does not exist");
        
        var dtos = await GetOperationTimeAsync(request, well, token);
        return dtos;
    }

    private async Task<IEnumerable<SubsystemOperationTimeDto>> GetOperationTimeAsync(SubsystemOperationTimeRequest request, WellDto well, CancellationToken token)
    {
        var query = BuildQuery(request, well);
        IEnumerable<SubsystemOperationTime> data = await query.ToListAsync(token);

        if (request.SelectMode == SubsystemOperationTimeRequest.SelectModeInner)
        {
            if (request.GtDate is not null)
                data = data.Where(o => o.DateStart >= request.GtDate.Value);

            if (request.LtDate is not null)
                data = data.Where(o => o.DateEnd <= request.LtDate.Value);
        }
        else if (request.SelectMode == SubsystemOperationTimeRequest.SelectModeTrim)
        {
            var begin = request.GtDate?.ToUtcDateTimeOffset(well.Timezone.Hours);
            var end = request.LtDate?.ToUtcDateTimeOffset(well.Timezone.Hours);
            data = TrimOperation(data, begin, end);
        }

        var dtos = data.Select(o => Convert(o, well.Timezone.Hours));
        return dtos;
    }

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

        request.SelectMode = SubsystemOperationTimeRequest.SelectModeTrim;
        var subsystemsTimes = await GetOperationTimeAsync(request, well, token);
        if (subsystemsTimes is null)
            return Enumerable.Empty<SubsystemStatDto>();

        var detectedOperationSummaryRequest = new DetectedOperationSummaryRequest()
        {
            IdsTelemetries = new[] {well.IdTelemetry!.Value},
            IdsOperationCategories = WellOperationCategory.MechanicalDrillingSubIds,

            GeDateStart = request.GtDate,
            LeDateStart = request.LtDate,
            
            GeDepthStart = request.GtDepth,
            LeDepthStart = request.LtDepth,
        };
        var operationsSummaries = await detectedOperationService.GetOperationSummaryAsync(detectedOperationSummaryRequest, token);
        if(!operationsSummaries.Any())
            return Enumerable.Empty<SubsystemStatDto>();

        var statList = CalcStat(subsystemsTimes, operationsSummaries);
        return statList;
    }

    private static IEnumerable<SubsystemOperationTime> TrimOperation(IEnumerable<SubsystemOperationTime> data, DateTimeOffset? gtDate, DateTimeOffset? ltDate)
    {
        if (!ltDate.HasValue && !gtDate.HasValue) 
            return data.Select(d => d.Adapt<SubsystemOperationTime>());

        var items = data.Select((item) => 
        {
            var operationTime = item.Adapt<SubsystemOperationTime>();
            if (!(item.DepthStart.HasValue && item.DepthEnd.HasValue))
                return operationTime;

            var dateDiff = (item.DateEnd - item.DateStart).TotalSeconds;
            var depthDiff = item.DepthEnd.Value - item.DepthStart.Value;
            var a = depthDiff / dateDiff;               
            var b = item.DepthStart.Value;

            if (gtDate.HasValue && item.DateStart < gtDate.Value)
            {
                operationTime.DateStart = gtDate.Value;
                var x = (gtDate.Value - item.DateStart).TotalSeconds;
                operationTime.DepthStart = (float)(a * x + b);
            }
            if (ltDate.HasValue && item.DateEnd > ltDate.Value)
            {
                operationTime.DateEnd = ltDate.Value;
                var x = (ltDate.Value - item.DateStart).TotalSeconds;
                operationTime.DepthEnd = (float)(a * x + b);
            }
            return operationTime;
        });

        return items;
    }

    private IEnumerable<SubsystemStatDto> CalcStat(
        IEnumerable<SubsystemOperationTimeDto> subsystemsTimes,
        IEnumerable<OperationsSummaryDto> operationsSummaries)
    {
        var groupedSubsystemsTimes = subsystemsTimes
            .OrderBy(o => o.Id)
            .GroupBy(o => o.IdSubsystem);

        var periodGroupTotal = subsystemsTimes.Sum(o => (o.DateEnd - o.DateStart).TotalHours);

        var result = groupedSubsystemsTimes.Select(g =>
        {
            var periodGroup = g.Sum(o => (o.DateEnd - o.DateStart).TotalHours);
            var periodGroupDepth = g.Sum(o => o.DepthEnd - o.DepthStart);
            var (sumOprationsDepth, sumOprationsDurationHours) = AggregateOperationsSummaries(g.Key, operationsSummaries);
            var subsystemStat = new SubsystemStatDto()
            {
                IdSubsystem = g.Key,
                SubsystemName = subsystemService.GetOrDefault(g.Key)?.Name ?? "unknown",
                UsedTimeHours = periodGroup,
                SumOperationDepthInterval = sumOprationsDepth,
                SumOperationDurationHours = sumOprationsDurationHours,
                SumDepthInterval = periodGroupDepth,
                KUsage = periodGroupDepth / sumOprationsDepth,
                OperationCount = g.Count(),
            };
            if (subsystemStat.KUsage > 1)
                subsystemStat.KUsage = 1;
            return subsystemStat;
        });

        var apdParts = result.Where(x => x.IdSubsystem == 11 || x.IdSubsystem == 12);
        if (apdParts.Any())
        {
            var apdSum = new SubsystemStatDto()
            {
                IdSubsystem = IdSubsystemAKB,
                SubsystemName = "АПД",
                UsedTimeHours = apdParts.Sum(part => part.UsedTimeHours),
                SumOperationDepthInterval = apdParts.Sum(part => part.SumOperationDepthInterval),
                SumOperationDurationHours = apdParts.Sum(part => part.SumOperationDurationHours),
                SumDepthInterval = apdParts.Sum(part => part.SumDepthInterval),
                OperationCount = apdParts.Sum(part => part.OperationCount),
            };
            apdSum.KUsage = apdSum.SumDepthInterval / apdSum.SumOperationDepthInterval;
            if (apdSum.KUsage > 1)
                apdSum.KUsage = 1;
            result = result.Append(apdSum).OrderBy(m => m.IdSubsystem);
        }

        return result;
    }

    private static (double SumDepth, double SumDurationHours) AggregateOperationsSummaries(int idSubsystem, IEnumerable<OperationsSummaryDto> operationsSummaries)
        => idSubsystem switch
        {
            IdSubsystemAKBRotor or IdSubsystemTorque => CalcOperationSummariesByCategories(operationsSummaries, WellOperationCategory.IdRotor),
            IdSubsystemAKBSlide or IdSubsystemSpin => CalcOperationSummariesByCategories(operationsSummaries, WellOperationCategory.IdSlide),
            IdSubsystemAKB or IdSubsystemMSE => CalcOperationSummariesByCategories(operationsSummaries, WellOperationCategory.IdRotor, WellOperationCategory.IdSlide),
            _ => throw new ArgumentException($"idSubsystem: {idSubsystem} does not supported in this method", nameof(idSubsystem)),
        };

    private static (double SumDepth, double SumDurationHours) CalcOperationSummariesByCategories(
        IEnumerable<OperationsSummaryDto> operationsSummaries,
        params int[] idsOperationCategories)
    {
        var filtered = operationsSummaries.Where(sum => idsOperationCategories.Contains(sum.IdCategory));
        var sumDepth = filtered.Sum(summ => summ.SumDepthIntervals);
        var sumDurationHours = filtered.Sum(summ => summ.SumDurationHours);
        return (sumDepth, sumDurationHours);
    }

    /// <inheritdoc/>
    public async Task<IEnumerable<SubsystemActiveWellStatDto>> GetStatByActiveWells(int idCompany, DateTime? gtDate, DateTime? ltDate, CancellationToken token)
    {
        var activeWells = await wellService.GetAsync(new() { IdCompany = idCompany, IdState = 1 }, token);
        var result = await GetStatAsync(activeWells, gtDate, ltDate, token);
        return result;
    }

    /// <inheritdoc/>
    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;
    }

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

        var hoursOffset = wells
            .FirstOrDefault(well => well.Timezone is not null)
            ?.Timezone.Hours
            ?? 5d;

        var beginUTC = gtDate.HasValue
            ? gtDate.Value.ToUtcDateTimeOffset(hoursOffset)
            : db.SubsystemOperationTimes.Min(s => s.DateStart)
                .DateTime
                .ToUtcDateTimeOffset(hoursOffset);

        var endUTC = ltDate.HasValue
            ? ltDate.Value.ToUtcDateTimeOffset(hoursOffset)
            : db.SubsystemOperationTimes.Max(s => s.DateEnd)
                .DateTime
                .ToUtcDateTimeOffset(hoursOffset);

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

        var query = db.SubsystemOperationTimes
            .Where(o => idsTelemetries.Contains(o.IdTelemetry) &&
                        o.DateStart >= beginUTC &&
                        o.DateEnd <= endUTC)
            .AsNoTracking();

        var subsystemsOperationTime = await query.ToArrayAsync(token);

        var operationSummaries = await detectedOperationService
            .GetOperationSummaryAsync(new ()
            {
                IdsTelemetries = idsTelemetries,
                IdsOperationCategories = WellOperationCategory.MechanicalDrillingSubIds,
                GeDateStart = beginUTC,
                LeDateEnd = endUTC,
            }, token);

        var result = wells
            .Select(well => {
                var dtos = subsystemsOperationTime
                    .Where(s => s.IdTelemetry == well.IdTelemetry)
                    .Select(s => Convert(s, well.Timezone.Hours));
                                    
                var wellStat = new SubsystemActiveWellStatDto{ Well = well };

                var telemetryOperationSummaries = operationSummaries.Where(summ => summ.IdTelemetry == well.IdTelemetry);
                if (telemetryOperationSummaries.Any())
                {
                    var subsystemStat = CalcStat(dtos, telemetryOperationSummaries);
                    if (subsystemStat.Any())
                    {
                        wellStat.SubsystemAKB = subsystemStat.FirstOrDefault(s => s.IdSubsystem == IdSubsystemAKB);
                        wellStat.SubsystemMSE = subsystemStat.FirstOrDefault(s => s.IdSubsystem == IdSubsystemMSE);
                        wellStat.SubsystemSpinMaster = subsystemStat.FirstOrDefault(s => s.IdSubsystem == IdSubsystemSpin);
                        wellStat.SubsystemTorqueMaster = subsystemStat.FirstOrDefault(s => s.IdSubsystem == IdSubsystemTorque);
                    }
                }

                return wellStat;
            });

        return result;
    }

    /// <inheritdoc/>
    public async Task<DatesRangeDto?> GetDateRangeOperationTimeAsync(SubsystemOperationTimeRequest request, CancellationToken token)
    {
        var well = await wellService.GetOrDefaultAsync(request.IdWell, token)
            ?? throw new ArgumentInvalidException(nameof(request.IdWell), $"Well Id: {request.IdWell} does not exist");

        var query = BuildQuery(request, well);
        if (query is null)
        {
            return null;
        }
        var result = await query
            .GroupBy(o => o.IdTelemetry)
            .Select(g => new DatesRangeDto
            {
                From = g.Min(o => o.DateStart).DateTime,
                To = g.Max(o => o.DateEnd).DateTime
            })
            .FirstOrDefaultAsync(token);
        return result;
    }

    private IQueryable<SubsystemOperationTime> BuildQuery(SubsystemOperationTimeRequest request, WellDto well)
    {
        var idTelemetry = well.IdTelemetry
            ?? throw new ArgumentInvalidException(nameof(request.IdWell), $"Well Id: {request.IdWell} has no telemetry");

        var query = db.SubsystemOperationTimes
            .Include(o => o.Subsystem)
            .Where(o => o.IdTelemetry == idTelemetry)
            .AsNoTracking();

        if (request.IdsSubsystems.Any())
            query = query.Where(o => request.IdsSubsystems.Contains(o.IdSubsystem));

        // # Dates range condition
        //           [GtDate                  LtDate]
        //      [DateStart   DateEnd]  [DateStart  DateEnd]
        if (request.GtDate.HasValue)
        {
            DateTimeOffset gtDate = request.GtDate.Value.ToUtcDateTimeOffset(well.Timezone.Hours);
            query = query.Where(o => o.DateEnd >= gtDate);
        }

        if (request.LtDate.HasValue)
        {
            DateTimeOffset ltDate = request.LtDate.Value.ToUtcDateTimeOffset(well.Timezone.Hours);
            query = query.Where(o => o.DateStart <= ltDate);
        }

        if (request.GtDepth.HasValue)
            query = query.Where(o => o.DepthEnd >= request.GtDepth.Value);

        if (request.LtDepth.HasValue)
            query = query.Where(o => o.DepthStart <= request.LtDepth.Value);

        if (request?.SortFields?.Any() == true)
        {
            query = query.SortBy(request.SortFields);
        }
        else
        {
            query = query
                .OrderBy(o => o.DateStart)
                .ThenBy(o => o.DepthStart);
        }

        if (request?.Skip > 0)
            query = query.Skip((int)request.Skip);

        if (request?.Take > 0)
            query = query.Take((int)request.Take);

        return query;
    }

    private static SubsystemOperationTimeDto Convert(SubsystemOperationTime operationTime, double? timezoneHours = null)
    {
        var dto = operationTime.Adapt<SubsystemOperationTimeDto>();
        var hours = timezoneHours ?? operationTime.Telemetry.TimeZone.Hours;
        dto.DateStart = operationTime.DateStart.ToRemoteDateTime(hours);
        dto.DateEnd = operationTime.DateEnd.ToRemoteDateTime(hours);
        return dto;
    }        
}