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;

#nullable enable
namespace AsbCloudInfrastructure.Services.SAUB
{
    public abstract class TelemetryDataBaseService<TDto, TEntity> : ITelemetryDataService<TDto>
        where TDto : AsbCloudApp.Data.ITelemetryData
        where TEntity : class, ITelemetryData
    {
        protected readonly IAsbCloudDbContext db;
        private readonly ITelemetryService telemetryService;
        private readonly TelemetryDataCache<TDto> telemetryDataCache;

        public TelemetryDataBaseService(
            IAsbCloudDbContext db,
            ITelemetryService telemetryService,
            TelemetryDataCache<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 idTelemetry = telemetryService.GetOrCreateTelemetryIdByUid(uid);
            var timezone = telemetryService.GetTimezone(idTelemetry);

            telemetryDataCache.AddRange(idTelemetry, dtos);

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

            var entityMaxDate = entities.Max(e => e.DateTime);
            telemetryService.TelemetryTracker.SaveRequestDate(uid, entityMaxDate);

            var dbset = db.Set<TEntity>();
            var stopwatch = Stopwatch.StartNew();
            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 {idTelemetry}, " +
                    $"count: {entities.Count()}, " +
                    $"dataDate: {entities.FirstOrDefault()?.DateTime}, " +
                    $"dbSaveDurationTime:{stopwatch.ElapsedMilliseconds}ms. " +
                    $"Message: {ex.Message}");
                return 0;
            }
        }


        // TODO: It shouldn`t be nullable. Throw exceptions instead and return empty.
        /// <inheritdoc/>
        public virtual async Task<IEnumerable<TDto>?> GetOrDefaultAsync(int idWell,
            DateTime dateBegin = default, double intervalSec = 600d,
            int approxPointsCount = 1024, CancellationToken token = default)
        {
            var idTelemetry = telemetryService.GetOrDefaultIdTelemetryByIdWell(idWell) ?? -1;
            if (idTelemetry == -1)
                return null;

            var timezone = telemetryService.GetTimezone(idTelemetry);

            var filterByDateEnd = dateBegin != default;
            DateTimeOffset dateBeginUtc;
            if (dateBegin == default)
            {
                dateBeginUtc = telemetryService.GetLastTelemetryDate(idTelemetry)
                    .ToUtcDateTimeOffset(timezone.Hours);
                if (dateBeginUtc != default)
                    dateBeginUtc = dateBeginUtc.AddSeconds(-intervalSec);
            }
            else
            {
                dateBeginUtc = dateBegin.ToUtcDateTimeOffset(timezone.Hours);
            }

            if (dateBeginUtc == default)
                dateBeginUtc = DateTime.UtcNow.AddSeconds(-intervalSec);

            var cacheData = telemetryDataCache.GetOrDefault(idTelemetry, 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 == idTelemetry
                    && d.DateTime >= dateBeginUtc);

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

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

            if (fullDataCount == 0)
                return default;

            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
                .OrderBy(d => d.DateTime)
                .AsNoTracking()
                .ToListAsync(token)
                .ConfigureAwait(false);

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

            return dtos;
        }

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

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

    }
}
#nullable disable