using AsbCloudDb.Model;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Mapster;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using AsbCloudInfrastructure.Background;
using System.Threading;
using AsbCloudApp.Data;

namespace AsbCloudInfrastructure.Services.SAUB
{
    public class TelemetryDataCache<TDto>
        where TDto : AsbCloudApp.Data.ITelemetryData
    {
        class TelemetryDataCacheItem
        {
            public TDto? FirstByDate { get; init; }
            public CyclycArray<TDto> LastData { get; init; } = null!;
        }

        private IServiceProvider provider = null!;
        private const int activeWellCapacity = 12 * 60 * 60;
        private const int doneWellCapacity = 65 * 60;

        // key == idTelemetry
        private readonly ConcurrentDictionary<int, TelemetryDataCacheItem> caches;
        private bool isLoading = false;

        private TelemetryDataCache()
        {
            caches = new();
        }

        private static TelemetryDataCache<TDto>? instance;

        public static TelemetryDataCache<TDto> GetInstance<TEntity>(IServiceProvider provider)
            where TEntity : class, AsbCloudDb.Model.ITelemetryData
        {
            if (instance is null)
            {
                instance = new TelemetryDataCache<TDto>();
                var worker = provider.GetRequiredService<BackgroundWorker>();
                var workId = $"Telemetry cache loading from DB {typeof(TEntity).Name}";
                var work = new WorkBase(workId, async (workId, provider, token) => {
                    var db = provider.GetRequiredService<IAsbCloudDbContext>();
                    await instance.InitializeCacheFromDBAsync<TEntity>(db, token);
                });

                worker.Push(work);
            }
            instance.provider = provider;
            return instance;
        }

        /// <summary>
        /// Добавить новые элементы в кеш
        /// </summary>
        /// <param name="idTelemetry"></param>
        /// <param name="range"></param>
        public void AddRange(int idTelemetry, IEnumerable<TDto> range)
        {
            if (!range.Any())
                return;

            var newItems = range
                .OrderBy(i => i.DateTime);

            foreach (var item in newItems)
                item.IdTelemetry = idTelemetry;

            TelemetryDataCacheItem cacheItem;
            if (isLoading)
            {
                if (caches.TryGetValue(idTelemetry, out TelemetryDataCacheItem? localCacheItem))
                    cacheItem = localCacheItem;
                else
                    return;
            }
            else
            {
                cacheItem = caches.GetOrAdd(idTelemetry, _ => new TelemetryDataCacheItem() 
                { 
                    FirstByDate = newItems.ElementAt(0),
                    LastData = new CyclycArray<TDto>(activeWellCapacity) 
                });
            }
            
            cacheItem.LastData.AddRange(newItems);
        }

        /// <summary>
        /// Получить данные из кеша. <br/>
        /// Если dateBegin меньше минимального элемента в кеше, то вернется null.
        /// Даже если intervalSec частично перекрыт данными из кеша.
        /// </summary>
        /// <param name="idTelemetry"></param>
        /// <param name="dateBegin"></param>
        /// <param name="intervalSec"></param>
        /// <param name="approxPointsCount">кол-во элементов до которых эти данные прореживаются</param>
        /// <returns></returns>
        public IEnumerable<TDto>? GetOrDefault(int idTelemetry, DateTime dateBegin, double intervalSec = 600d, int approxPointsCount = 1024)
        {
            if(!caches.TryGetValue(idTelemetry, out TelemetryDataCacheItem? cacheItem))            
                return null;

            var cacheLastData = cacheItem.LastData;

            if (!cacheLastData.Any() || cacheLastData[0].DateTime > dateBegin)
                return null;
            
            var dateEnd = dateBegin.AddSeconds(intervalSec);
            var items = cacheLastData
                .Where(i => i.DateTime >= dateBegin && i.DateTime <= dateEnd);

            var ratio = items.Count() / approxPointsCount;
            if (ratio > 1)
                items = items
                    .Where((_, index) => index % ratio == 0);

            return items;
        }

        public TDto? GetLastOrDefault(int idTelemetry)
        {
            if (!caches.TryGetValue(idTelemetry, out TelemetryDataCacheItem? cacheItem))
                return default;

            return cacheItem.LastData.LastOrDefault();
        }

        public DatesRangeDto? GetOrDefaultDataDateRange(int idTelemetry)
        {
            if (!caches.TryGetValue(idTelemetry, out TelemetryDataCacheItem? cacheItem))
                return null;

            var from = cacheItem.FirstByDate?.DateTime;
            if(!cacheItem.LastData.Any())
                return null;

            var to = cacheItem.LastData[^1].DateTime;
            from = from ?? cacheItem.LastData[0].DateTime;

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

        private async Task InitializeCacheFromDBAsync<TEntity>(IAsbCloudDbContext db, CancellationToken token)
            where TEntity : class, AsbCloudDb.Model.ITelemetryData
        {
            if (isLoading)
                throw new Exception("Multiple cache loading detected.");

            isLoading = true;
            Well[] wells = Array.Empty<Well>();

            wells = await db.Set<Well>()
                .Include(well => well.Telemetry)
                .Include(well => well.Cluster)
                .Where(well => well.IdTelemetry != null)
                .ToArrayAsync(token);

            foreach (Well well in wells)
            {
                var capacity = well.IdState == 1
                    ? activeWellCapacity
                    : doneWellCapacity;

                var idTelemetry = well.IdTelemetry!.Value;
                var hoursOffset = well.Timezone.Hours;

                var cacheItem = await GetOrDefaultCacheDataFromDbAsync<TEntity>(db, idTelemetry, capacity, hoursOffset, token);
                if(cacheItem is not null)
                {
                    caches.TryAdd(idTelemetry, cacheItem);
                    System.Diagnostics.Trace.TraceInformation($"cache<{typeof(TDto).Name}> for well: {well.Cluster?.Caption}/{well.Caption} loaded");
                }
                else
                {
                    System.Diagnostics.Trace.TraceInformation($"cache<{typeof(TDto).Name}> for well: {well.Cluster?.Caption}/{well.Caption} has no data");
                }
            }

            System.Diagnostics.Trace.TraceInformation($"cache<{typeof(TDto).Name}> load complete");
            isLoading = false;
        }

        private static async Task<TelemetryDataCacheItem?> GetOrDefaultCacheDataFromDbAsync<TEntity>(IAsbCloudDbContext db, int idTelemetry, int capacity, double hoursOffset, CancellationToken token)
            where TEntity : class, AsbCloudDb.Model.ITelemetryData
        {
            var query = db.Set<TEntity>()
                .Where(i => i.IdTelemetry == idTelemetry);

            var firstDbEntity = await query
                .OrderBy(i => i.DateTime)
                .FirstOrDefaultAsync(token);

            if (firstDbEntity is null)
                return default;

            var first = firstDbEntity.Adapt<TDto>();
            first.DateTime = firstDbEntity.DateTime.ToRemoteDateTime(hoursOffset);

            var entities = await query
                .OrderByDescending(i => i.DateTime)
                .Take(capacity)
                .ToArrayAsync(token);

            var dtos = entities
                .AsEnumerable()
                .Reverse()
                .Select(entity => {
                    var dto = entity.Adapt<TDto>();
                    dto.DateTime = entity.DateTime.ToRemoteDateTime(hoursOffset);
                    return dto;
                });

            var cacheItem = new CyclycArray<TDto>(capacity);
            cacheItem.AddRange(dtos);

            var item = new TelemetryDataCacheItem
            {
                FirstByDate = first,
                LastData = cacheItem,
            };
            return item;
        }
    }
}