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>();
}