using AsbCloudApp.Comparators;
using AsbCloudApp.Data;
using AsbCloudApp.Data.User;
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;


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(nameof(names), "Invalid role 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(nameof(dto), "Invalid include role (self reference)");

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