using AsbCloudApp.Comparators;
using AsbCloudApp.Data;
using AsbCloudApp.Exceptions;
using AsbCloudApp.Repositories;
using AsbCloudDb;
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.Repository
{
#nullable enable
    public class UserRoleRepository : IUserRoleRepository
    {
        private readonly IAsbCloudDbContext dbContext;
        private readonly IMemoryCache memoryCache;

        public UserRoleRepository(IAsbCloudDbContext dbContext, IMemoryCache memoryCache)
        {
            this.dbContext = dbContext;
            this.memoryCache = memoryCache;
        }

        public async Task<int> InsertAsync(UserRoleDto dto, CancellationToken token)
        {
            var entity = dto.Adapt<UserRole>();
            var updatedEntity = dbContext.UserRoles.Add(entity);
            await dbContext.SaveChangesAsync(token);

            if (updatedEntity.IsKeySet)
            {
                dto.Id = updatedEntity.Entity.Id;
                await UpdatePermissionsAsync(dto, token);
                await UpdateIncludedRolesAsync(dto, token);

                DropCacheUserRole();
                return updatedEntity.Entity.Id;
            }
            return 0;
        }

        public Task<int> InsertRangeAsync(IEnumerable<UserRoleDto> newItems, CancellationToken token)
        {
            throw new NotImplementedException();
        }

        public async Task<IEnumerable<UserRoleDto>> GetAllAsync(CancellationToken token)
        {
            var entities = await  GetCacheUserRoleAsync(token)
                .ConfigureAwait(false);

            return entities.Select(Convert);
        }

        public UserRoleDto? GetOrDefault(int id)
        {
            var entity = GetCacheUserRole().FirstOrDefault(x => x.Id == id);
            if (entity is null)
                return null;
            return Convert(entity);
        }

        public async Task<UserRoleDto?> GetOrDefaultAsync(int id, CancellationToken token)
        {
            var entity = (await GetCacheUserRoleAsync(token)
                .ConfigureAwait(false)).FirstOrDefault(r => r.Id == id);
            if (entity is null)
                return null;
            return Convert(entity);
        }

        public async Task<IEnumerable<UserRoleDto>> GetByNamesAsync(IEnumerable<string> names, CancellationToken token)
        {
            if (names?.Any() != true)
                return Enumerable.Empty<UserRoleDto>();

            var entities = (await GetCacheUserRoleAsync(token))
                .Where(r => names.Contains(r.Caption));

            if (entities?.Count() != names.Count())
                throw new ArgumentInvalidException("Invalid role names", nameof(names));

            return entities.Select(Convert);
        }

        public async Task<int> UpdateAsync(UserRoleDto dto, CancellationToken token)
        {
            await UpdatePermissionsAsync(dto, token);
            await UpdateIncludedRolesAsync(dto, token);

            var entity = Convert(dto);
            var result = dbContext.UserRoles.Upsert(entity);
            await dbContext.SaveChangesAsync(token);
            DropCacheUserRole();
            return result.Entity.Id;
        }

        public IEnumerable<UserRoleDto> GetNestedById(int id, int recursionLevel = 7)
        {
            var role = GetCacheUserRole()
                .FirstOrDefault(r => r.Id == id);
            if (role is null)
                return Enumerable.Empty<UserRoleDto>();

            var roles = new SortedSet<UserRoleDto>(ComparerIId.GetInstance()) { Convert(role) };

            if (recursionLevel <= 0 || role.RelationUserRoleUserRoles?.Any() != true)
                return roles;

            foreach (var relation in role.RelationUserRoleUserRoles)
            {
                var nestedRoles = GetNestedById(relation.IdInclude, --recursionLevel);
                if (nestedRoles?.Any() != true)
                    continue;
                foreach (var nestedRole in nestedRoles)
                    roles.Add(nestedRole);
            }
            return roles;
        }

        public async Task<int> DeleteAsync(int id, CancellationToken token)
        {
            var entity = (await GetCacheUserRoleAsync(token)).FirstOrDefault(r => r.Id == id);

            if (entity is not null)
            {
                var removeEntity = dbContext.UserRoles.Remove(entity);
                await dbContext.SaveChangesAsync(token);
                DropCacheUserRole();
                return removeEntity.Entity.Id;
            }
            else return 0;
        }

        public bool HasPermission(IEnumerable<int> rolesIds, string permissionName)
        {
            var permissionInfo = GetCacheRelationUserRolePermissions()
                .FirstOrDefault(p => p. Permission?.Name.ToLower() == permissionName.ToLower())
                ?.Permission;

            if (permissionInfo is null)
                return false;

            if (rolesIds.Contains(1))
                return true;

            var idPermissionInfo = permissionInfo.Id;
            var entities = GetCacheUserRole()
                .Where(r => rolesIds.Contains(r.Id));

            foreach (var role in entities)
                if (HasPermission(role, idPermissionInfo))
                    return true;
            return false;
        }

        private bool HasPermission(UserRole userRole, int idPermission, int recursionLevel = 7)
        {
            if (userRole.RelationUserRolePermissions.Any(p => p.IdPermission == idPermission))
                return true;

            if (recursionLevel <= 0 || userRole.RelationUserRoleUserRoles?.Any() != true)
                return false;

            foreach (var relation in userRole.RelationUserRoleUserRoles)
            {
                var entity = GetCacheUserRole()
                    .First(p => p.Id == relation.IdInclude);
                if (HasPermission(entity, idPermission, --recursionLevel))
                    return true;
            }
            return false;
        }

        private IEnumerable<UserRoleDto> GetNestedByIds(IEnumerable<int> ids, int recursionLevel = 7)
        { 
            var roles = new List<UserRoleDto>();
            foreach (var id in ids)
                roles.AddRange(GetNestedById(id, recursionLevel));

            return roles;
        }

        private async Task UpdateIncludedRolesAsync(UserRoleDto dto, CancellationToken token)
        {
            if (!dto.Roles.Any())
                return;

            var idsIncludeRole = GetNestedByIds(dto.Roles.Select(x => x.Id)).Select(x => x.Id);

            if (idsIncludeRole.Any(x => x == dto.Id))
                throw new ArgumentInvalidException("Invalid include role (self reference)", nameof(dto));

            var removeRelationsQuery = dbContext.RelationUserRoleUserRoles
                .Where(r => r.Id == dto.Id);

            dbContext.RelationUserRoleUserRoles.RemoveRange(removeRelationsQuery);
            var newRelations = dto.Roles.Select(r => new RelationUserRoleUserRole 
            { 
                Id = dto.Id, 
                IdInclude = r.Id,
            });
            dbContext.RelationUserRoleUserRoles.AddRange(newRelations);
            await dbContext.SaveChangesAsync(token);
            DropCacheRelationUserRoleUserRole();
        }

        private async Task UpdatePermissionsAsync(UserRoleDto dto, CancellationToken token)
        {
            if (dto?.Permissions is null)
                return;

            var relations = await dbContext.RelationUserRolePermissions
                .Where(r => r.IdUserRole == dto.Id)
                .ToListAsync(token)
                .ConfigureAwait(false);
            dbContext.RelationUserRolePermissions.RemoveRange(relations);

            if (dto.Permissions.Any())
            {
                var newRelations = dto.Permissions.Select(p => new RelationUserRolePermission
                {
                    IdPermission = p.Id,
                    IdUserRole = dto.Id,
                });

                await dbContext.RelationUserRolePermissions.AddRangeAsync(newRelations, token);
                await dbContext.SaveChangesAsync(token);
            }
            DropCacheRelationUserRolePermissions();
        }

        private Task<IEnumerable<UserRole>> GetCacheUserRoleAsync(CancellationToken token)
            => memoryCache.GetOrCreateBasicAsync(dbContext.Set<UserRole>()
            .Include(r => r.RelationUserRolePermissions)
            .Include(r => r.RelationUserRoleUserRoles)
            .Include(r => r.RelationUsersUserRoles), token);

        private IEnumerable<UserRole> GetCacheUserRole()
            => memoryCache.GetOrCreateBasic(dbContext.Set<UserRole>()
            .Include(r => r.RelationUserRolePermissions)
            .Include(r => r.RelationUserRoleUserRoles)
            .Include(r => r.RelationUsersUserRoles));

        private void DropCacheUserRole()
            => memoryCache.DropBasic<UserRole>();

        private void DropCacheRelationUserRoleUserRole()
            => memoryCache.DropBasic<RelationUserUserRole>();

        private IEnumerable<RelationUserRolePermission> GetCacheRelationUserRolePermissions()
            => memoryCache.GetOrCreateBasic(dbContext.Set<RelationUserRolePermission>()
            .Include(r => r.UserRole)
            .Include(r => r.Permission));

        private void DropCacheRelationUserRolePermissions()
            => memoryCache.DropBasic<RelationUserRolePermission>();

        private UserRoleDto Convert(UserRole entity)
        {
            var dto = entity.Adapt<UserRoleDto>();
            if (entity.RelationUserRolePermissions?.Any() == true)
            {
                dto.Permissions = GetCacheRelationUserRolePermissions()
                    .Where(r => entity.Id == r.IdUserRole)
                    .Select(r => Convert(r.Permission));
            }

            if (entity.RelationUserRoleUserRoles?.Any() == true)
            {
                var rolesCache = GetCacheUserRole();
                dto.Roles = entity.RelationUserRoleUserRoles
                    .Select(rel => Convert(rolesCache
                        .First(r => r.Id == rel.IdInclude)))
                    .ToArray();
            }
            return dto;
        }

        private static PermissionDto Convert(Permission entity)
        {
            var dto = entity.Adapt<PermissionDto>();
            return dto;
        }

        private static UserRole Convert(UserRoleDto dto)
        {
            var entity = dto.Adapt<UserRole>();
            return entity;
        }
    }
#nullable disable
}