using AsbCloudApp.Data;
using AsbCloudApp.Exceptions;
using AsbCloudApp.Services;
using AsbCloudDb.Model;
using Mapster;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace AsbCloudInfrastructure.Services
{

    public class MeasureService : IMeasureService
    {
        private readonly IAsbCloudDbContext db;
        private readonly IMemoryCache memoryCache;
        private readonly IWellService wellService;
        private static readonly TimeSpan CacheOlescence = TimeSpan.FromMinutes(20);

        public MeasureService(IAsbCloudDbContext db, IMemoryCache memoryCache, IWellService wellService)
        {
            this.db = db;
            this.memoryCache = memoryCache;
            this.wellService = wellService;
        }

        public async Task<Dictionary<int, string>> GetCategoriesAsync(CancellationToken token)
        {
            var key = typeof(MeasureCategory).FullName;
            var cache = await memoryCache.GetOrCreateAsync(key, async (cacheEntry) => {
                cacheEntry.AbsoluteExpirationRelativeToNow = CacheOlescence;
                cacheEntry.SlidingExpiration = CacheOlescence;
                var entities = await db.Set<MeasureCategory>()
                    .ToDictionaryAsync(e => e.Id, e => e.Name, token);
                return entities;
            });
            return cache;
        }

        public async Task<MeasureDto?> GetLastOrDefaultAsync(int idWell, int idCategory, CancellationToken token)
        {
            var query = db.Measures
                .Include(m => m.Category)
                .Where(m => m.IdWell == idWell && m.IdCategory == idCategory && !m.IsDeleted)
                .OrderByDescending(m => m.Timestamp)
                .Take(1);

            var entity = await query
                .AsNoTracking()
                .FirstOrDefaultAsync(token)
                .ConfigureAwait(false);

            var timezone = wellService.GetTimezone(idWell);

            if (entity is null)
                return null;

            return Convert(entity, timezone.Hours);
        }

        public async Task<IEnumerable<MeasureDto>> GetHisoryAsync(int idWell, int? idCategory = null, CancellationToken token = default)
        {
            var query = db.Measures.Include(m => m.Category)
                .Where(m => m.IdWell == idWell && !m.IsDeleted);

            if (idCategory is not null)
                query = query.Where(m => m.IdCategory == idCategory);

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

            var timezone = wellService.GetTimezone(idWell);
            var dtos = entities.Select(e => Convert(e, timezone.Hours));
            return dtos;
        }

        public Task<int> InsertAsync(int idWell, MeasureDto dto, CancellationToken token)
        {
            if (dto.IdCategory < 1)
                throw new ArgumentInvalidException(nameof(dto), "wrong idCategory");
            if (!dto.Data.Any())
                throw new ArgumentInvalidException(nameof(dto), "data.data is not optional");
            var entity = Convert(dto);
            entity.IdWell = idWell;
            db.Measures.Add(entity);
            return db.SaveChangesAsync(token);
        }

        public async Task<int> UpdateAsync(int idWell, MeasureDto dto, CancellationToken token)
        {
            if (dto.Id < 1)
                throw new ArgumentInvalidException(nameof(dto), "wrong id");
            if (dto.IdCategory < 1)
                throw new ArgumentInvalidException(nameof(dto), "wrong idCategory");
            if (!dto.Data.Any())
                throw new ArgumentInvalidException(nameof(dto), "data.data is not optional");

            var entity = await db.Measures
                .Where(m => m.Id == dto.Id && !m.IsDeleted)
                .FirstOrDefaultAsync(token)
                ?? throw new ArgumentInvalidException(nameof(dto), "id doesn't exist");

            var timezone = wellService.GetTimezone(idWell);
            entity.IdWell = idWell;
            entity.Timestamp = dto.Timestamp.ToOffset(TimeSpan.FromHours(timezone.Hours));
            entity.Data = dto.Data.Adapt<RawData>();

            return await db.SaveChangesAsync(token).ConfigureAwait(false);
        }

        public async Task<int> MarkAsDeleteAsync(int idWell, int idData, CancellationToken token)
        {
            if (idData < 1)
                throw new ArgumentInvalidException(nameof(idData), "wrong id");

            var entity = await db.Measures.Where(m => m.IdWell == idWell && m.Id == idData)
                .FirstOrDefaultAsync(token)
                ?? throw new ArgumentInvalidException(nameof(idWell), $"Measure doesn't exist");

            entity.IsDeleted = true;

            return await db.SaveChangesAsync(token).ConfigureAwait(false);
        }

        public Task<int> DeleteAsync(int idWell, int idData, CancellationToken token)
        {
            if (idData < 1)
                throw new ArgumentInvalidException(nameof(idData), "wrong id");
            db.Measures.RemoveRange(db.Measures.Where(m => m.IdWell == idWell && m.Id == idData));
            return db.SaveChangesAsync(token);
        }

        private MeasureDto Convert(Measure entity, double hours)
        {
            var dto = entity.Adapt<MeasureDto>();
            dto.CategoryName = entity.Category?.Name ?? String.Empty;
            dto.Timestamp = entity.Timestamp.ToOffset(TimeSpan.FromHours(hours));
            return dto;
        }
        private Measure Convert(MeasureDto dto)
        {
            var entity = dto.Adapt<Measure>();
            entity.Timestamp = dto.Timestamp.ToUniversalTime();
            return entity;
        }
    }

}