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

namespace AsbCloudInfrastructure.Services.DetectOperations
{
#nullable enable
    public static class OperationDetectionWorkFactory
    {
        private const string workId = "Operation detection";
        private static readonly TimeSpan workPeriod = TimeSpan.FromMinutes(30);
        private static string progress = "no progress";

        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 static WorkPeriodic MakeWork() 
        {            
            var workPeriodic = new WorkPeriodic(workId, WorkAction, workPeriod);
            workPeriodic.Timeout = TimeSpan.FromSeconds(15 * 60);
            workPeriodic.OnErrorAsync = (id, exception, token) =>
            {
                var text = $"work {id}, when {progress}, throw error:{exception.Message}";
                Trace.TraceWarning(text);
                return Task.CompletedTask;
            };
            return workPeriodic;
        }

        // TODO: Разделить этот акшн на более мелкие части И использовать telemetryServiceData<..> вместо прямого обращения к БД.
        private static async Task WorkAction(string _, IServiceProvider serviceProvider, CancellationToken token)
        {
            using var db = serviceProvider.GetRequiredService<IAsbCloudDbContext>();

            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 joinedlastDetectedDates = telemetryIds
                .GroupJoin(lastDetectedDates,
                    t => t,
                    o => o.IdTelemetry,
                    (outer, inner) => new
                    {
                        IdTelemetry = outer,
                        inner.SingleOrDefault()?.LastDate,
                    });

            var affected = 0;
            foreach (var item in joinedlastDetectedDates)
            {
                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);
                }
            }
        }

        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)
                .Where(d => d.BlockPosition >= 0)
                .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;
                var step = 10;
                while (positionEnd > positionBegin)
                {
                    step ++;
                    for (int i = 0; i < detectors.Length; i++)
                    {
                        progress = $"telemetry:{idTelemetry}, date:{startDate}, pos:{positionBegin}, detector:{detectors[i]}";
                        if (detectors[i].TryDetect(idTelemetry, data, positionBegin, positionEnd, lastDetectedOperation, out OperationDetectorResult? result))
                        {
                            detectedOperations.Add(result!.Operation);
                            lastDetectedOperation = result.Operation;
                            isDetected = true;
                            step = 1;
                            positionBegin = result.TelemetryEnd;
                            break;
                        }
                    }
                    if (step > 20)
                        step = 10;
                    positionBegin += step;
                }

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

            return detectedOperations;
        }
    }
#nullable disable
}