using AsbCloudApp.Data;
using AsbCloudApp.Exceptions;
using AsbCloudApp.Repositories;
using AsbCloudApp.Services;
using AsbCloudDb;
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 AsbCloudApp.Requests;

namespace AsbCloudInfrastructure.Services.SAUB
{
    public abstract class TelemetryDataBaseService<TDto, TEntity> : ITelemetryDataService<TDto>
        where TDto : AsbCloudApp.Data.ITelemetryData
        where TEntity : class, AsbCloudDb.Model.ITelemetryData
    {
        protected readonly IAsbCloudDbContext db;
        protected readonly ITelemetryService telemetryService;
        protected readonly ITelemetryDataCache<TDto> telemetryDataCache;

        protected TelemetryDataBaseService(
            IAsbCloudDbContext db,
            ITelemetryService telemetryService,
            ITelemetryDataCache<TDto> telemetryDataCache)
        {
            this.db = db;
            this.telemetryService = telemetryService;
            this.telemetryDataCache = telemetryDataCache;
        }

        /// <inheritdoc/>
        public virtual async Task<int> UpdateDataAsync(string uid, IEnumerable<TDto> dtos, CancellationToken token = default)
        {
            if (dtos == default || !dtos.Any())
                return 0;

            var dtosList = dtos.OrderBy(d => d.DateTime).ToList();

            var dtoMinDate = dtosList.First().DateTime;
            var dtoMaxDate = dtosList.Last().DateTime;

            if (dtosList.Count > 1)
            {
                var duplicates = new List<TDto>(8);
                for (int i = 1; i < dtosList.Count; i++)
                    if (dtosList[i].DateTime - dtosList[i - 1].DateTime < TimeSpan.FromMilliseconds(100))
                        duplicates.Add(dtosList[i - 1]);
                foreach (var duplicate in duplicates)
                    dtosList.Remove(duplicate);
            }

            var telemetry = telemetryService.GetOrCreateTelemetryByUid(uid);
            var timezone = telemetryService.GetTimezone(telemetry.Id);

            telemetryDataCache.AddRange(telemetry.Id, dtos);

            var entities = dtosList.Select(dto =>
            {
                var entity = Convert(dto, timezone.Hours);
                entity.IdTelemetry = telemetry.Id;
                return entity;
            });

            var stopwatch = Stopwatch.StartNew();
            var dbset = db.Set<TEntity>();
            try
            {
                return await db.Database.ExecInsertOrUpdateAsync(dbset, entities, token).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                stopwatch.Stop();
                Trace.WriteLine($"Fail to save data telemetry " +
                    $"uid: {uid}, " +
                    $"idTelemetry {telemetry.Id}, " +
                    $"count: {entities.Count()}, " +
                    $"dataDate: {entities.FirstOrDefault()?.DateTime}, " +
                    $"dbSaveDurationTime:{stopwatch.ElapsedMilliseconds}ms. " +
                    $"Message: {ex.Message}");
                return 0;
            }
        }

        /// <inheritdoc/>
        public virtual async Task<IEnumerable<TDto>> GetByWellAsync(int idWell,
            DateTime dateBegin = default, double intervalSec = 600d,
            int approxPointsCount = 1024, CancellationToken token = default)
        {
            var telemetry = telemetryService.GetOrDefaultTelemetryByIdWell(idWell);
            if (telemetry is null)
                return Enumerable.Empty<TDto>();

            var timezone = telemetryService.GetTimezone(telemetry.Id);

            var filterByDateEnd = dateBegin != default;
            DateTimeOffset dateBeginUtc;
            if (dateBegin == default)
            {
                var dateRange = telemetryDataCache.GetOrDefaultDataDateRange(telemetry.Id);
                dateBeginUtc = (dateRange?.To ?? DateTimeOffset.UtcNow)
                    .AddSeconds(-intervalSec);
            }
            else
            {
                dateBeginUtc = dateBegin.ToUtcDateTimeOffset(timezone.Hours);
            }

            var cacheData = telemetryDataCache.GetOrDefault(telemetry.Id, dateBeginUtc.ToRemoteDateTime(timezone.Hours), intervalSec, approxPointsCount);
            if (cacheData is not null)
                return cacheData;

            var dateEnd = dateBeginUtc.AddSeconds(intervalSec);
            var dbSet = db.Set<TEntity>();

            var query = dbSet
                .Where(d => d.IdTelemetry == telemetry.Id
                    && d.DateTime >= dateBeginUtc);

            if (filterByDateEnd)
                query = query.Where(d => d.DateTime <= dateEnd);

            var fullDataCount = await query.CountAsync(token)
                .ConfigureAwait(false);

            if (fullDataCount == 0)
                return Enumerable.Empty<TDto>();

            if (fullDataCount > 1.75 * approxPointsCount)
            {
                var m = (int)Math.Round(1d * fullDataCount / approxPointsCount);
                if (m > 1)
                    query = query.Where((d) => (((d.DateTime.DayOfYear * 24 + d.DateTime.Hour) * 60 + d.DateTime.Minute) * 60 + d.DateTime.Second) % m == 0);
            }

            var entities = await query
                .AsNoTracking()
                .ToArrayAsync(token);

            var dtos = entities.Select(e => Convert(e, timezone.Hours));

            return dtos;
        }

        /// <inheritdoc/>
        public virtual async Task<IEnumerable<TDto>> GetByWellAsync(int idWell, TelemetryDataRequest request, CancellationToken token)
        {
            var telemetry = telemetryService.GetOrDefaultTelemetryByIdWell(idWell);
            if (telemetry is null)
                return Enumerable.Empty<TDto>();

            return await GetByTelemetryAsync(telemetry.Id, request, token);
        }

        public async Task<IEnumerable<TDto>> GetByTelemetryAsync(int idTelemetry, TelemetryDataRequest request, CancellationToken token)
        {
            var timezone = telemetryService.GetTimezone(idTelemetry);
            
            var cache = telemetryDataCache.GetOrDefault(idTelemetry, request);
            
            if(cache is not null)
                return cache;
            
            var query = BuildQuery(idTelemetry, request);

            var entities = await query
                .AsNoTracking()
                .ToArrayAsync(token);

            var dtos = entities.Select(e => Convert(e, timezone.Hours));

            return dtos;
        }

        private IQueryable<TEntity> BuildQuery(int idTelemetry, TelemetryDataRequest request)
        {
            var dbSet = db.Set<TEntity>();

            var query = dbSet
                .Where(d => d.IdTelemetry == idTelemetry);

            if (request.GeDate.HasValue)
            {
                var geDate = request.GeDate.Value.UtcDateTime;
                query = query.Where(d => d.DateTime >= geDate);
            }

            if (request.LeDate.HasValue)
            {
                var leDate = request.LeDate.Value.UtcDateTime;
                query = query.Where(d => d.DateTime <= leDate);
            }

            if (request.Divider > 1)
                query = query.Where((d) => (((d.DateTime.DayOfYear * 24 + d.DateTime.Hour) * 60 + d.DateTime.Minute) * 60 + d.DateTime.Second) % request.Divider == 0);

            switch (request.Order)
            {
                case 1:// Поздние вперед
                    query = query
                        .OrderByDescending(d => d.DateTime)
                        .Skip(request.Skip)
                        .Take(request.Take)
                        .OrderBy(d => d.DateTime);
                    break;
                default:// Ранние вперед
                    query = query
                        .OrderBy(d => d.DateTime)
                        .Skip(request.Skip)
                        .Take(request.Take);
                    break;
            }

            return query;
        }

        /// <inheritdoc/>
        public async Task<DatesRangeDto?> GetRangeAsync(int idWell, DateTimeOffset geDate, DateTimeOffset? leDate, CancellationToken token)
        {
            var telemetry = telemetryService.GetOrDefaultTelemetryByIdWell(idWell) 
                ?? throw new ArgumentInvalidException(nameof(idWell), $"По скважине id:{idWell} нет телеметрии");

            if ((DateTimeOffset.UtcNow - geDate) < TimeSpan.FromHours(12))
            {
                // пробуем обойтись кешем
                var cechedRange = telemetryDataCache.GetOrDefaultCachedDateRange(telemetry.Id);
                if (cechedRange?.From <= geDate)
                {
                    var datesRange = new DatesRangeDto
                    {
                        From = geDate.DateTime,
                        To = cechedRange.To
                    };
                    if (leDate.HasValue && leDate > geDate)
                        datesRange.To = leDate.Value.Date;
                    return datesRange;
                }
            }

            var query = db.Set<TEntity>()
                .Where(entity => entity.IdTelemetry == telemetry.Id)
                .Where(entity => entity.DateTime >= geDate.ToUniversalTime());

            if(leDate.HasValue)
                query = query.Where(entity => entity.DateTime <= leDate.Value.ToUniversalTime());

            var gquery = query
                .GroupBy(entity => entity.IdTelemetry)
                .Select(group => new
                {
                    MinDate = group.Min(entity => entity.DateTime),
                    MaxDate = group.Max(entity => entity.DateTime),
                });

            var result = await gquery.FirstOrDefaultAsync(token);
            if (result is null)
                return null;

            var range = new DatesRangeDto
            {
                From = result.MinDate.ToOffset(TimeSpan.FromHours(telemetry.TimeZone!.Hours)).DateTime,
                To = result.MaxDate.ToOffset(TimeSpan.FromHours(telemetry.TimeZone!.Hours)).DateTime,
            };
            return range;
        }

        public DatesRangeDto? GetRange(int idWell)
        {
            var telemetry = telemetryService.GetOrDefaultTelemetryByIdWell(idWell);
            if (telemetry is null)
                return default;

            return telemetryDataCache.GetOrDefaultDataDateRange(telemetry.Id);
        }

        protected abstract TDto Convert(TEntity src, double timezoneOffset);

        protected abstract TEntity Convert(TDto src, double timezoneOffset);

    }
}