using AsbCloudApp.Services;
using AsbCloudDb.Model;
using Mapster;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AsbCloudApp.Exceptions;
using Microsoft.EntityFrameworkCore.ChangeTracking;

namespace AsbCloudInfrastructure.Repository
{

    /// <summary>
    /// CRUD сервис для работы с БД
    /// </summary>
    /// <typeparam name="TDto"></typeparam>
    /// <typeparam name="TEntity"></typeparam>
    public class CrudRepositoryBase<TDto, TEntity> : QueryContainer<TEntity>, ICrudRepository<TDto>
        where TDto : AsbCloudApp.Data.IId
        where TEntity : class, IId
    {
        public CrudRepositoryBase(IAsbCloudDbContext context)
            : base(context)
        { }

        public CrudRepositoryBase(IAsbCloudDbContext context, Func<DbSet<TEntity>, IQueryable<TEntity>> makeQuery)
            : base(context, makeQuery)
        { }

        /// <inheritdoc/>
        public virtual async Task<IEnumerable<TDto>> GetAllAsync(CancellationToken token)
        {
            var entities = await GetQuery()
                .AsNoTracking()
                .ToListAsync(token)
                .ConfigureAwait(false);
            var dtos = entities.Select(Convert).ToList();
            return dtos;
        }

        /// <inheritdoc/>
        public virtual async Task<TDto?> GetOrDefaultAsync(int id, CancellationToken token)
        {
            var entity = await GetQuery()
                .AsNoTracking()
                .FirstOrDefaultAsync(e => e.Id == id, token)
                .ConfigureAwait(false);
            if (entity == default)
                return default;
            var dto = Convert(entity);
            return dto;
        }       

        /// <inheritdoc/>
        public virtual TDto? GetOrDefault(int id)
        {
            var entity = GetQuery()
                .AsNoTracking()
                .FirstOrDefault(e => e.Id == id);
            if (entity == default)
                return default;
            var dto = Convert(entity);
            return dto;
        }

        /// <inheritdoc/>
        public virtual async Task<int> InsertAsync(TDto item, CancellationToken token)
        {
            var entity = Convert(item);
            entity.Id = 0;
            var entry = dbSet.Add(entity);
            await dbContext.SaveChangesAsync(token);
            entry.State = EntityState.Detached;
            return entity.Id;
        }

        /// <inheritdoc/>
        public virtual async Task<int> InsertRangeAsync(IEnumerable<TDto> items, CancellationToken token)
        {
            if (!items.Any())
                return 0;
            var entities = items.Select(i =>
            {
                var entity = Convert(i);
                entity.Id = 0;
                return entity;
            });
            var entries = new List<EntityEntry>(items.Count());
            foreach (var entity in entities)
            {
                var entry = dbSet.Add(entity);
                entries.Add(entry);
            }
            var affected = await dbContext.SaveChangesAsync(token);
            entries.ForEach(e => e.State = EntityState.Detached);
            return affected;
        }

        /// <inheritdoc/>
        public virtual async Task<int> UpdateAsync(TDto item, CancellationToken token)
        {
            var existingEntity = await dbSet
                .AsNoTracking()
                .FirstOrDefaultAsync(e => e.Id == item.Id, token)
                .ConfigureAwait(false);

            if (existingEntity is null)
                return ICrudRepository<TDto>.ErrorIdNotFound;

            var entity = Convert(item);
            var entry = dbSet.Update(entity);
            await dbContext.SaveChangesAsync(token);
            entry.State = EntityState.Detached;
            return entry.Entity.Id;
        }
        
        public virtual async Task<int> UpdateRangeAsync(IEnumerable<TDto> dtos, CancellationToken token)
        {
            if (!dtos.Any())
                return 0;
            
            var ids = dtos
                .Select(o => o.Id)
                .Distinct()
                .ToArray();
            
            if (ids.Any(id => id == default))
                throw new ArgumentInvalidException(nameof(dtos), "Все записи должны иметь Id");
            
            if (ids.Length != dtos.Count())
                throw new ArgumentInvalidException(nameof(dtos), "Все записи должны иметь уникальные Id");
            
            var existingEntitiesCount = await dbContext.Set<TEntity>()
                .Where(o => ids.Contains(o.Id))
                .CountAsync(token);
            
            if (ids.Length != existingEntitiesCount)
                throw new ArgumentInvalidException(nameof(dtos), "Все записи должны существовать в БД");

            var entities = dtos.Select(Convert);
            
            var entries = entities.Select(entity => dbContext.Set<TEntity>().Update(entity)).ToList();

            var affected = await dbContext.SaveChangesAsync(token);
            
            entries.ForEach(entry => entry.State = EntityState.Detached);

            return affected;
        }

        /// <inheritdoc/>
        public virtual Task<int> DeleteAsync(int id, CancellationToken token)
        {
            var entity = dbSet
                .AsNoTracking()
                .FirstOrDefault(e => e.Id == id);
            if (entity == default)
                return Task.FromResult(ICrudRepository<TDto>.ErrorIdNotFound);
            dbSet.Remove(entity);
            var affected = dbContext.SaveChangesAsync(token);
            return affected;
        }

        public virtual async Task<int> DeleteRangeAsync(IEnumerable<int> ids, CancellationToken token)
        {
            if (!ids.Any())
                return 0;
            
            var countExistingEntities = await dbSet
                .Where(d => ids.Contains(d.Id))
                .CountAsync(token);

            if (ids.Count() > countExistingEntities)
                return ICrudRepository<TDto>.ErrorIdNotFound;
            
            var entities = dbContext.Set<TEntity>().Where(e => ids.Contains(e.Id));
            dbContext.Set<TEntity>().RemoveRange(entities);
            return await dbContext.SaveChangesAsync(token);
        }

        protected virtual TDto Convert(TEntity src) => src.Adapt<TDto>();

        protected virtual TEntity Convert(TDto src) => src.Adapt<TEntity>();
    }

}