using AsbCloudDb.Model;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AsbCloudInfrastructure.Services.DetectOperations.Detectors;

namespace AsbCloudInfrastructure.Services.DetectOperations
{
#nullable enable
    public class OperationDetectionBackgroundService : BackgroundService
    {
        private readonly string connectionString;
        private readonly TimeSpan period = TimeSpan.FromHours(1);

        private static readonly DetectorAbstract[] detectors = new DetectorAbstract[]
        {
            new DetectorRotor(),
            new DetectorSlide(),
            new DetectorDevelopment(),
            new DetectorTemplating(),
            new DetectorSlipsTime(),
            new DetectorStaticSurveying(),
            new DetectorFlashingBeforeConnection(),
            new DetectorFlashing(),
            new DetectorTemplatingWhileDrilling(),
        };

        public OperationDetectionBackgroundService(IConfiguration configuration)
        {
            connectionString = configuration.GetConnectionString("DefaultConnection");
        }

        protected override async Task ExecuteAsync(CancellationToken token = default)
        {
            var timeToStartAnalysis = DateTime.Now;
            var options = new DbContextOptionsBuilder<AsbCloudDbContext>()
                .UseNpgsql(connectionString)
                .Options;

            while (!token.IsCancellationRequested)
            {
                if (DateTime.Now > timeToStartAnalysis)
                {
                    timeToStartAnalysis = DateTime.Now + period;
                    try
                    {
                        using var context = new AsbCloudDbContext(options);
                        var added = await DetectedAllTelemetriesAsync(context, token);
                        Trace.TraceInformation($"Total detection complete. Added {added} operations.");
                    }
                    catch (Exception ex)
                    {
                        Trace.TraceError(ex.Message);
                    }
                    GC.Collect();
                }

                var ms = (int)(timeToStartAnalysis - DateTime.Now).TotalMilliseconds;
                ms = ms > 100 ? ms : 100;
                await Task.Delay(ms, token).ConfigureAwait(false);
            }
        }

        public override async Task StopAsync(CancellationToken token)
        {
            await base.StopAsync(token).ConfigureAwait(false);
        }

        private static async Task<int> DetectedAllTelemetriesAsync(IAsbCloudDbContext db, CancellationToken token)
        {
            var lastDetectedDates = await db.DetectedOperations
                .GroupBy(o => o.IdTelemetry)
                .Select(g => new
                {
                    IdTelemetry = g.Key,
                    LastDate = g.Max(o => o.DateEnd)
                })
                .ToListAsync(token);

            var telemetryIds = await db.Telemetries
                .Where(t => t.Info != null && t.TimeZone != null)
                .Select(t => t.Id)
                .ToListAsync(token);

            var JounedlastDetectedDates = telemetryIds
                .GroupJoin(lastDetectedDates,
                    t => t,
                    o => o.IdTelemetry,
                    (outer, inner) => new
                    {
                        IdTelemetry = outer,
                        inner.SingleOrDefault()?.LastDate,
                    });
            var affected = 0;
            foreach (var item in JounedlastDetectedDates)
            {
                var stopwatch = Stopwatch.StartNew();
                var newOperations = await DetectOperationsAsync(item.IdTelemetry, item.LastDate ?? DateTimeOffset.MinValue, db, token);
                stopwatch.Stop();
                if (newOperations.Any())
                {
                    db.DetectedOperations.AddRange(newOperations);
                    affected += await db.SaveChangesAsync(token);
                }
            }
            return affected;
        }

        private static async Task<IEnumerable<DetectedOperation>> DetectOperationsAsync(int idTelemetry, DateTimeOffset begin, IAsbCloudDbContext db, CancellationToken token)
        {
            var query = db.TelemetryDataSaub
                .AsNoTracking()
                .Where(d => d.IdTelemetry == idTelemetry)
                .Select(d => new DetectableTelemetry
                {
                    DateTime = d.DateTime,
                    IdUser = d.IdUser,
                    WellDepth = d.WellDepth ?? float.NaN,
                    Pressure = d.Pressure ?? float.NaN,
                    HookWeight = d.HookWeight ?? float.NaN,
                    BlockPosition = d.BlockPosition ?? float.NaN,
                    BitDepth = d.BitDepth ?? float.NaN,
                    RotorSpeed = d.RotorSpeed ?? float.NaN,
                })
                .OrderBy(d => d.DateTime);

            var take = 4 * 86_400; // 4 дня
            var startDate = begin;
            var detectedOperations = new List<DetectedOperation>(8);
            DetectedOperation? lastDetectedOperation = null;
            const int minOperationLength = 5;
            const int maxDetectorsInterpolationFrameLength = 30;
            const int gap = maxDetectorsInterpolationFrameLength + minOperationLength;

            while (true)
            {
                var data = await query
                    .Where(d => d.DateTime > startDate)
                    .Take(take)
                    .ToArrayAsync(token);

                if (data.Length < gap)
                    break;

                var isDetected = false;
                var positionBegin = 0;
                var positionEnd = data.Length - gap;
                while (positionEnd > positionBegin)
                {
                    for (int i = 0; i < detectors.Length; i++)
                    {
                        if (detectors[i].TryDetect(idTelemetry, data, positionBegin, positionEnd, lastDetectedOperation, out OperationDetectorResult? result))
                        {
                            detectedOperations.Add(result!.Operation);
                            lastDetectedOperation = result.Operation;
                            isDetected = true;
                            positionBegin = result.TelemetryEnd;
                            break;
                        }
                    }
                    positionBegin++;
                }

                if (isDetected)
                    startDate = lastDetectedOperation!.DateEnd;
                else
                    startDate = data[positionEnd].DateTime;
            }

            return detectedOperations;
        }
    }
#nullable disable
}