using AsbCloudApp.Data;
using AsbCloudApp.Services;
using AsbCloudDb.Model;
using AsbCloudDb;
using AsbCloudInfrastructure.Services.Cache;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace AsbCloudInfrastructure.Services
{
    public abstract class TelemetryDataBaseService<TDto, TModel> : ITelemetryDataService<TDto>, IConverter<TDto, TModel>
        where TDto : AsbCloudApp.Data.ITelemetryData
        where TModel : class, AsbCloudDb.Model.ITelemetryData
    {
        private readonly IAsbCloudDbContext db;
        private readonly ITelemetryService telemetryService;
        protected readonly CacheTable<Telemetry> cacheTelemetry;
        protected readonly CacheTable<TelemetryUser> cacheTelemetryUsers;
        protected readonly CacheTable<Well> cacheWells;

        public TelemetryDataBaseService(
            IAsbCloudDbContext db,
            ITelemetryService telemetryService,
            CacheDb cacheDb)
        {
            this.db = db;
            this.telemetryService = telemetryService;
            cacheTelemetry = cacheDb.GetCachedTable<Telemetry>((AsbCloudDbContext)db);
            cacheTelemetryUsers = cacheDb.GetCachedTable<TelemetryUser>((AsbCloudDbContext)db);
            cacheWells = cacheDb.GetCachedTable<Well>((AsbCloudDbContext)db);
        }

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

            var idTelemetry = telemetryService.GetOrCreateTelemetryIdByUid(uid);
            var lastTelemetryDate = telemetryService.GetLastTelemetryDate(uid);
            var dtosList = dtos.OrderBy(d => d.Date).ToList();

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

            if (dtosList.Count > 1)
            {
                var duplicates = new List<TDto>(8);
                for (int i = 1; i < dtosList.Count; i++)
                    if (dtosList[i].Date - dtosList[i-1].Date < TimeSpan.FromMilliseconds(100))
                        duplicates.Add(dtosList[i - 1]);
                foreach (var duplicate in duplicates)
                    dtosList.Remove(duplicate);
            }
            
            var offsetHours = await telemetryService.GetTelemetryTimeZoneOffsetAsync(idTelemetry, token); 

            var entities = dtosList.Select(d => {
                    var e = Convert(d);
                    e.IdTelemetry = idTelemetry;
                    if(offsetHours is not null)
                        e.Date = telemetryService.TimeZoneService.DateToUtc(d.Date, (double)offsetHours);
                    return e;
                });

            var dbset = db.Set<TModel>();
            try
            {
                return await db.Database.ExecInsertOrUpdateAsync(dbset, entities, token).ConfigureAwait(false);
            }
            catch(Exception ex)
            {
                Trace.WriteLine($"Fail to save data telemerty uid: {uid}, idTelemetry {idTelemetry} count: {entities.Count()} dataDate: {entities.FirstOrDefault()?.Date}. Message: {ex.Message}");
                return 0;
            }
        }

        public virtual async Task<IEnumerable<TDto>> GetAsync(int idWell,
            DateTime dateBegin = default, double intervalSec = 600d,
            int approxPointsCount = 1024, bool isUtc = false, CancellationToken token = default)
        {
            var well = cacheWells.FirstOrDefault(w => w.Id == idWell);
            if (well?.IdTelemetry is null)
                return default;

            var idTelemetry = well?.IdTelemetry ?? default;

            var filterByDateEnd = dateBegin != default;
            if (dateBegin == default)
            {
                dateBegin = telemetryService.GetLastTelemetryDate(idTelemetry);
                if (dateBegin != default)
                    dateBegin = dateBegin.AddSeconds(-intervalSec);
            }                

            if (dateBegin == default)
                dateBegin = DateTime.Now.AddSeconds(-intervalSec);
            
            if (dateBegin.Kind == DateTimeKind.Unspecified)
                dateBegin = DateTime.SpecifyKind(dateBegin, DateTimeKind.Utc);

            var timeOffset = await telemetryService.GetTelemetryTimeZoneOffsetAsync(idTelemetry, token)
                .ConfigureAwait(false);

            if(timeOffset is not null)
                dateBegin = telemetryService.TimeZoneService.DateToUtc(dateBegin, timeOffset?? default);

            var dateEnd = dateBegin.AddSeconds(intervalSec);
            var dbSet = db.Set<TModel>();

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

            if (filterByDateEnd)
                query = query.Where(d => d.Date < 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);

                switch (m)
                {
                    //case var i when i <= 1: // тут для полноты, но никогда не сработает из-за условия выше
                    //    break;
                    case var i when i < 10:
                        query = query.Where((d) => d.Date.Second % m == 0);
                        break;
                    case var i when i < 30:
                        query = query.Where((d) => (d.Date.Minute * 60 + d.Date.Second) % m == 0);
                        break;
                    case var i when i < 600:
                        query = query.Where((d) => ((d.Date.Hour * 60 + d.Date.Minute) * 60 + d.Date.Second) % m == 0);
                        break;
                    default:
                        query = query.Where((d) => (((d.Date.DayOfYear * 24 + d.Date.Hour) * 60 + d.Date.Minute) * 60 + d.Date.Second) % m == 0);
                        break;
                }

                if (m > 1)
                    query = query.Where((d) => (((d.Date.DayOfYear*24 + d.Date.Hour)*60 + d.Date.Minute)*60 + d.Date.Second) % m == 0);
            }

            var entities = await query
                .OrderBy(d=>d.Date)
                .AsNoTracking()
                .ToListAsync(token)
                .ConfigureAwait(false);

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

            if (isUtc) 
                return dtos;

            if (timeOffset is null)
                return dtos;

            dtos = dtos.Select(d =>
            {
                d.Date = telemetryService.TimeZoneService.DateToTimeZone(d.Date, timeOffset ?? default);
                return d;
            });
                
            return dtos;
        }

        public virtual async Task<DatesRangeDto> GetDataDatesRangeAsync(int idWell, bool isUtc,
            CancellationToken token = default)
        {
            var telemetryId = telemetryService.GetIdTelemetryByIdWell(idWell);
            if (telemetryId is null)
                return null;

            var telemetry = await cacheTelemetry.FirstOrDefaultAsync(t => t.Id == telemetryId, token)
                .ConfigureAwait(false);

            var dto = telemetryService.TelemetryTracker.GetTelemetryDateRangeByUid(telemetry.RemoteUid);

            if (isUtc) 
                return dto;

            dto = await telemetryService.FixDatesRangeByTimeZoneAsync((int)telemetryId, dto, token)
                .ConfigureAwait(false);

            return dto;

        }

        public abstract TDto Convert(TModel src);

        public abstract TModel Convert(TDto src);
    }
}