using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace AsbCloudInfrastructure.Services.Cache
{
    public class CacheTable<TEntity> : IEnumerable<TEntity>
        where TEntity : class
    {
        private readonly DbContext context;
        private (DateTime refreshDate, IEnumerable<object> entities) data;
        private readonly List<TEntity> cached;
        private readonly DbSet<TEntity> dbSet;

        internal CacheTable(DbContext context, (DateTime refreshDate, IEnumerable<object> entities) data)
        {
            this.context = context;
            this.data = data;
            this.cached = (List<TEntity>)data.entities;
            dbSet = context.Set<TEntity>();
        }

        public TEntity this[int index] { get => cached.ElementAt(index); }

        public int Refresh()
        {
            if (cached.Any())
            {
                try
                {
                    cached.Clear();
                }
                catch
                {
                    // ignore
                    // TODO: figure out what the hell
                }

            }
            var dbEntities = context.Set<TEntity>().AsNoTracking().ToList();
            cached.AddRange(dbEntities);
            data.refreshDate = DateTime.Now;
            return cached.Count;
        }

        public async Task<int> RefreshAsync(CancellationToken token = default)
        {
            if (cached.Any())
            {
                try
                {
                    cached.Clear();
                }
                catch
                {
                    // ignore
                    // TODO: figure out what the well
                }
            }
            var dbEntities = await context.Set<TEntity>().AsNoTracking().ToListAsync(token).ConfigureAwait(false);
            cached.AddRange(dbEntities);
            data.refreshDate = DateTime.Now;
            return cached.Count;
        }

        private bool CheckRefresh(RefreshMode refreshMode)
        {
            if (refreshMode == RefreshMode.Force)
            {
                Refresh();
                return true;
            }

            if ((!cached.Any()) && (refreshMode == RefreshMode.IfResultEmpty))
            {
                Refresh();
                return true;
            }

            return false;
        }

        private async Task<bool> CheckRefreshAsync(RefreshMode refreshMode, CancellationToken token = default)
        {
            if (refreshMode == RefreshMode.Force)
            {
                await RefreshAsync(token);
                return true;
            }

            if ((!cached.Any()) && (refreshMode == RefreshMode.IfResultEmpty))
            {
                await RefreshAsync(token);
                return true;
            }

            return false;
        }

        public bool Contains(Func<TEntity, bool> predicate, RefreshMode refreshMode = RefreshMode.IfResultEmpty)
            => FirstOrDefault(predicate, refreshMode) != default;

        public Task<bool> ContainsAsync(Func<TEntity, bool> predicate, CancellationToken token = default)
            => ContainsAsync(predicate, RefreshMode.IfResultEmpty, token);

        public async Task<bool> ContainsAsync(Func<TEntity, bool> predicate, RefreshMode refreshMode = RefreshMode.IfResultEmpty, CancellationToken token = default)
            => await FirstOrDefaultAsync(predicate, refreshMode, token) != default;

        public Task<TEntity> FirstOrDefaultAsync(CancellationToken token = default)
            => FirstOrDefaultAsync(RefreshMode.IfResultEmpty, token);

        public TEntity FirstOrDefault(RefreshMode refreshMode = RefreshMode.IfResultEmpty)
        {
            bool isUpdated = CheckRefresh(refreshMode);
            var result = cached.FirstOrDefault();
            if (result == default && refreshMode == RefreshMode.IfResultEmpty && !isUpdated)
            {
                Refresh();
                return cached.FirstOrDefault();
            }
            return result;
        }

        public async Task<TEntity> FirstOrDefaultAsync(RefreshMode refreshMode = RefreshMode.IfResultEmpty, CancellationToken token = default)
        {
            bool isUpdated = await CheckRefreshAsync(refreshMode, token);
            var result = cached.FirstOrDefault();
            if (result == default && refreshMode == RefreshMode.IfResultEmpty && !isUpdated)
            {
                await RefreshAsync(token);
                return cached.FirstOrDefault();
            }
            return result;
        }

        public Task<TEntity> FirstOrDefaultAsync(Func<TEntity, bool> predicate, CancellationToken token = default)
            => FirstOrDefaultAsync(predicate, RefreshMode.IfResultEmpty, token);

        public TEntity FirstOrDefault(Func<TEntity, bool> predicate, RefreshMode refreshMode = RefreshMode.IfResultEmpty)
        {
            bool isUpdated = CheckRefresh(refreshMode);
            var result = cached.FirstOrDefault(predicate);
            if (result == default && refreshMode == RefreshMode.IfResultEmpty && !isUpdated)
            {
                Refresh();
                return cached.FirstOrDefault(predicate);
            }
            return result;
        }

        public async Task<TEntity> FirstOrDefaultAsync(Func<TEntity, bool> predicate, RefreshMode refreshMode = RefreshMode.IfResultEmpty, CancellationToken token = default)
        {
            bool isUpdated = await CheckRefreshAsync(refreshMode, token);
            var result = cached.FirstOrDefault(predicate);
            if (result == default && refreshMode == RefreshMode.IfResultEmpty && !isUpdated)
            {
                await RefreshAsync(token);
                return cached.FirstOrDefault(predicate);
            }
            return result;
        }

        public Task<IEnumerable<TEntity>> WhereAsync(CancellationToken token = default)
            => WhereAsync(default, RefreshMode.IfResultEmpty, token);

        public Task<IEnumerable<TEntity>> WhereAsync(Func<TEntity, bool> predicate, CancellationToken token = default)
            => WhereAsync(predicate, RefreshMode.IfResultEmpty, token);

        public IEnumerable<TEntity> Where(Func<TEntity, bool> predicate = default, RefreshMode refreshMode = RefreshMode.IfResultEmpty)
        {
            bool isUpdated = CheckRefresh(refreshMode);
            var result = (predicate != default)
                ? cached.Where(predicate)
                : cached;
            if (!result.Any() && refreshMode == RefreshMode.IfResultEmpty && !isUpdated)
            {
                Refresh();
                result = (predicate != default)
                    ? cached.Where(predicate)
                    : cached;
            }
            return result;
        }

        public async Task<IEnumerable<TEntity>> WhereAsync(Func<TEntity, bool> predicate = default,
            RefreshMode refreshMode = RefreshMode.IfResultEmpty, CancellationToken token = default)
        {
            bool isUpdated = await CheckRefreshAsync(refreshMode, token);
            var result = (predicate != default)
                ? cached.Where(predicate)
                : cached;
            if (!result.Any() && refreshMode == RefreshMode.IfResultEmpty && !isUpdated)
            {
                await RefreshAsync(token);
                result = (predicate != default)
                    ? cached.Where(predicate)
                    : cached;
            }
            return result;
        }

        public IEnumerable<TEntity> Mutate(Func<TEntity, bool> predicate, Action<TEntity> mutation)
        {
            var dbEntities = dbSet.Where(predicate);
            if (dbEntities.Any())
            {
                foreach (var dbEntity in dbEntities)
                    mutation(dbEntity);
                context.SaveChanges();
            }
            cached.RemoveAll(e => predicate(e));
            cached.AddRange(dbEntities);
            return dbEntities;
        }

        public async Task<IEnumerable<TEntity>> MutateAsync(Func<TEntity, bool> predicate, Action<TEntity> mutation, CancellationToken token = default)
        {
            var dbEntities = dbSet.Where(predicate);
            if (dbEntities.Any())
            {
                foreach (var dbEntity in dbEntities)
                    mutation(dbEntity);
                await context.SaveChangesAsync(token).ConfigureAwait(false);
            }
            cached.RemoveAll(e => predicate(e));
            cached.AddRange(dbEntities);
            return dbEntities;
        }

        public TEntity Upsert(TEntity entity)
        {
            var updated = dbSet.Update(entity);
            context.SaveChanges();
            Refresh();
            return updated.Entity;
        }

        public async Task<TEntity> UpsertAsync(TEntity entity, CancellationToken token = default)
        {
            Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry<TEntity> updated;
            if (dbSet.Contains(entity))
                updated = dbSet.Update(entity);
            else
                updated = dbSet.Add(entity);
            await context.SaveChangesAsync(token).ConfigureAwait(false);
            await RefreshAsync(token).ConfigureAwait(false);
            return updated.Entity;
        }

        public IEnumerable<TEntity> Upsert(IEnumerable<TEntity> entities)
        {
            var upsertedEntries = new List<TEntity>(entities.Count());
            foreach (var entity in entities)
            {
                Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry<TEntity> updated;
                if (dbSet.Contains(entity)) // TODO: это очень ммедленно
                    updated = dbSet.Update(entity);
                else
                    updated = dbSet.Add(entity);
                upsertedEntries.Add(updated.Entity);
            }
            context.SaveChanges();
            Refresh();
            return upsertedEntries;
        }

        public async Task<IEnumerable<TEntity>> UpsertAsync(IEnumerable<TEntity> entities, CancellationToken token = default)
        {
            var upsertedEntries = new List<TEntity>(entities.Count());
            foreach (var entity in entities)
            {
                Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry<TEntity> updated;
                if (dbSet.Contains(entity))
                    updated = dbSet.Update(entity);
                else
                    updated = dbSet.Add(entity);
                upsertedEntries.Add(updated.Entity);
            }
            await context.SaveChangesAsync(token).ConfigureAwait(false);
            await RefreshAsync(token).ConfigureAwait(false);
            return upsertedEntries;
        }

        public void Remove(Func<TEntity, bool> predicate)
        {
            cached.RemoveAll(e => predicate(e));
            dbSet.RemoveRange(dbSet.Where(predicate));
            context.SaveChanges();
            return;
        }

        public async Task RemoveAsync(Func<TEntity, bool> predicate, CancellationToken token = default)
        {
            cached.RemoveAll(e => predicate(e));
            dbSet.RemoveRange(dbSet.Where(predicate));
            await context.SaveChangesAsync(token).ConfigureAwait(false);
            return;
        }

        public TEntity Insert(TEntity entity)
        {
            var dbEntity = dbSet.Add(entity).Entity;
            context.SaveChanges();
            cached.Add(dbEntity);
            return dbEntity;
        }

        public async Task<TEntity> InsertAsync(TEntity entity, CancellationToken token = default)
        {
            var dbEntity = dbSet.Add(entity).Entity;
            await context.SaveChangesAsync(token).ConfigureAwait(false);
            cached.Add(dbEntity);
            return dbEntity;
        }

        public IEnumerable<TEntity> Insert(IEnumerable<TEntity> newEntities)
        {
            var dbEntities = new List<TEntity>(newEntities.Count());
            foreach (var item in newEntities)
                dbEntities.Add(dbSet.Add(item).Entity);
            context.SaveChanges();
            cached.AddRange(dbEntities);
            return dbEntities;
        }

        public async Task<IEnumerable<TEntity>> InsertAsync(IEnumerable<TEntity> newEntities, CancellationToken token = default)
        {
            var dbEntities = new List<TEntity>(newEntities.Count());
            foreach (var item in newEntities)
                dbEntities.Add(dbSet.Add(item).Entity);
            await context.SaveChangesAsync(token).ConfigureAwait(false);
            cached.AddRange(dbEntities);
            return dbEntities;
        }

        public IEnumerator<TEntity> GetEnumerator() => Where().GetEnumerator();

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
    }
}