using AsbCloudApp.Data;
using AsbCloudApp.Exceptions;
using AsbCloudApp.Repositories;
using AsbCloudDb;
using AsbCloudDb.Model;
using AsbCloudInfrastructure.EfCache;
using Mapster;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace AsbCloudInfrastructure.Repository
{
#nullable enable
    public class UserRepository : IUserRepository
    {
        private readonly IAsbCloudDbContext dbContext;
        private readonly IUserRoleRepository userRoleRepository;
        private const string userCacheTag = "User";
        private const string relationUserUserRoleCacheTag = "RelationUserUserRole";
        private static readonly TimeSpan cacheObsolence = TimeSpan.FromMinutes(15);
        private static readonly TypeAdapterConfig userTypeAdapterConfig = TypeAdapterConfig<UserExtendedDto, User>
            .NewConfig()
            .Ignore(dst => dst.Company,
                dst => dst.FileMarks,
                dst => dst.Files,
                dst => dst.RelationUsersUserRoles)
            .Config;

        public UserRepository(IAsbCloudDbContext dbContext, IUserRoleRepository userRoleRepository) { 
            this.dbContext = dbContext;
            this.userRoleRepository = userRoleRepository;
        }

        public async Task<int> InsertAsync(UserExtendedDto dto, CancellationToken token)
        {
            dto.Id = default;
            var entity = Convert(dto);
            await AssertLoginIsBusyAsync(dto.Login, token);
            var userRoles = await userRoleRepository.GetByNamesAsync(dto.RoleNames, token).ConfigureAwait(false);
            var updatedEntity = await dbContext.Users.AddAsync(entity, token).ConfigureAwait(false);
            await dbContext.SaveChangesAsync(token);

            if (userRoles?.Any() == true)
                await UpdateRolesCacheForUserAsync(updatedEntity.Entity.Id, userRoles, token);

            DropCacheUsers();
            return updatedEntity.Entity.Id;
        }

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

        public async Task<IEnumerable<UserExtendedDto>> GetAllAsync(CancellationToken token)
        {
            var dtos = (await GetCacheUserAsync(token)).ToList();
            if (dtos is null)
                return Enumerable.Empty<UserExtendedDto>();
            
            for (var i = 0; i < dtos.Count; i++)
                dtos[i].RoleNames = GetRolesNamesByIdUser(dtos[i].Id);
            return dtos;
        }

        public UserExtendedDto? GetOrDefault(int id)
        {
            var dto = GetCacheUser().FirstOrDefault(u => u.Id == id);
            if (dto is null)
                return null;
            var entity = Convert(dto);
            dto.RoleNames = GetRolesNamesByIdUser(dto.Id);
            return dto;
        }

        public async Task<UserExtendedDto?> GetOrDefaultAsync(int id, CancellationToken token)
        {
            var dto = (await GetCacheUserAsync(token)).FirstOrDefault(u => u.Id == id);
            if (dto is null)
                return null;
            
            dto.RoleNames = GetRolesNamesByIdUser(dto.Id);
            return dto;
        }

        public async Task<int> UpdateAsync(UserExtendedDto dto, CancellationToken token)
        {
            if (dto.Id <= 1)
                throw new ArgumentInvalidException($"Invalid id {dto.Id}. You can't edit this user.", nameof(dto));

            var oldUser = (await GetCacheUserAsync(token)).FirstOrDefault(u => u.Id == dto.Id);
            if (oldUser is null)
                return 0;

            if (oldUser.Login != dto.Login)
                await AssertLoginIsBusyAsync(dto.Login, token);

            var userRoles = await userRoleRepository.GetByNamesAsync(dto.RoleNames, token).ConfigureAwait(false);
            await UpdateRolesCacheForUserAsync(dto.Id, userRoles, token);

            var entity = Convert(dto);

            var result = dbContext.Users.Upsert(entity);
            await dbContext.SaveChangesAsync(token);
            DropCacheUsers();
            return result.Entity.Id;
        }

        public async Task<int> DeleteAsync(int id, CancellationToken token)
        {
            var dto = (await GetCacheUserAsync(token)).FirstOrDefault(u => u.Id == id);
            if (dto is null)
                return 0;

            var entity = Convert(dto);
            var result = dbContext.Users.Remove(entity);
            await dbContext.SaveChangesAsync(token);
            DropCacheUsers();
            return result.Entity.Id;
        }

        public IEnumerable<UserRoleDto> GetRolesByIdUser(int idUser, int nestedLevel = 0)
        {
            var roles = GetCachRelationUserUserRoleCacheTag().Where(r => r.IdUser == idUser);
            return roles.SelectMany(r => userRoleRepository.GetNestedById(r.IdUserRole, nestedLevel));
        }

        public IEnumerable<PermissionDto> GetNestedPermissions(int idUser)
        {
            var roles = GetRolesByIdUser(idUser, 7);
            if (roles is null)
                return Enumerable.Empty<PermissionDto>();
            var permissions = roles
                .Where(r => r.Permissions is not null)
                .SelectMany(r => r.Permissions);

            return permissions;
        }

        public bool HasPermission(int idUser, string permissionName)
        {
            if (idUser == 1)
                return true;

            var relationsToRoles = GetCachRelationUserUserRoleCacheTag()
                .Where(r => r.IdUser == idUser);
            if (relationsToRoles is null)
                return false;

            return userRoleRepository.HasPermission(relationsToRoles
                .Select(r => r.IdUserRole), permissionName);
        }

        private IEnumerable<string> GetRolesNamesByIdUser(int idUser)
        => GetRolesByIdUser(idUser, 7)
            .Select(r => r.Caption)
            .Distinct();

        private async Task AssertLoginIsBusyAsync(string login, CancellationToken token)
        {
            var existingUserDto = (await GetCacheUserAsync(token))
                .FirstOrDefault(u => u.Login.ToLower() == login.ToLower());

            if (existingUserDto is not null)
                throw new ArgumentInvalidException($"Login {login} is busy by {existingUserDto.MakeDisplayName()}, id{existingUserDto.Id}", nameof(login));
        }

        private Task<IEnumerable<UserExtendedDto>> GetCacheUserAsync(CancellationToken token)
            => dbContext.Users
            .Include(r => r.Company)
            .Include(r => r.RelationUsersUserRoles)
            .FromCacheAsync(userCacheTag, cacheObsolence, Convert, token);
        private IEnumerable<UserExtendedDto> GetCacheUser()
            => dbContext.Users
            .Include(r => r.Company)
            .Include(r => r.RelationUsersUserRoles)
            .FromCache(userCacheTag, cacheObsolence, Convert);
        private void DropCacheUsers()
            => dbContext.Users.DropCache(userCacheTag);

        private Task<IEnumerable<RelationUserUserRole>> GetCacheRelationUserUserRoleAsync(CancellationToken token)
            => dbContext.RelationUserUserRoles
            .Include(r => r.UserRole)
            .Include(r => r.User)
            .FromCacheAsync(relationUserUserRoleCacheTag, cacheObsolence, token);
        private IEnumerable<RelationUserUserRole> GetCachRelationUserUserRoleCacheTag()
            => dbContext.RelationUserUserRoles
            .Include(r => r.UserRole)
            .Include(r => r.User)
            .FromCache(relationUserUserRoleCacheTag, cacheObsolence);
        private void DropCacheRelationUserUserRoleCacheTag()
            => dbContext.RelationUserUserRoles.DropCache(relationUserUserRoleCacheTag);

        private async Task UpdateRolesCacheForUserAsync(int idUser, IEnumerable<UserRoleDto> newRoles, CancellationToken token)
        {
            var relations = (await GetCacheRelationUserUserRoleAsync(token)).Where(r => r.IdUser == idUser);
            dbContext.RelationUserUserRoles.RemoveRange(relations);

            if (newRoles?.Any() == true)
                await dbContext.RelationUserUserRoles.AddRangeAsync(newRoles.Select(role => new RelationUserUserRole
                {
                    IdUser = idUser,
                    IdUserRole = role.Id
                }), token).ConfigureAwait(false);

            await dbContext.SaveChangesAsync(token);
            DropCacheRelationUserUserRoleCacheTag();
        }

        protected virtual User Convert(UserExtendedDto dto)
        {
            var entity = dto.Adapt<User>(userTypeAdapterConfig);
            if (string.IsNullOrEmpty(entity.PasswordHash))
                entity.PasswordHash = dbContext.Users.FirstOrDefault(u => u.Id == dto.Id)?.PasswordHash;
            return entity;
        }

        protected virtual UserExtendedDto Convert(User entity)
        {
            var dto = entity.Adapt<UserExtendedDto>();
            return dto;
        }
    }
#nullable disable
}