using AsbCloudApp.Data;
using AsbCloudApp.Exceptions;
using AsbCloudApp.Repositories;
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 Microsoft.Extensions.Caching.Memory;
using AsbCloudApp.Data.User;

namespace AsbCloudInfrastructure.Repository
{

    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;
        private readonly IMemoryCache memoryCache;
        public UserRepository(IAsbCloudDbContext dbContext, IUserRoleRepository userRoleRepository, IMemoryCache memoryCache)
        {
            this.dbContext = dbContext;
            this.userRoleRepository = userRoleRepository;
            this.memoryCache = memoryCache;
        }

        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 users = await GetCacheUserAsync(token);
            if (users is null)
                return Enumerable.Empty<UserExtendedDto>();
            var dtos = new List<UserExtendedDto>();
            foreach(var user in users)
            {
                var dto = Convert(user);
                dto.RoleNames = GetRolesNamesByIdUser(user.Id);
                dtos.Add(dto);
            };            
            return dtos;
        }
        

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

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

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

            var oldUser = (await GetCacheUserAsync(token)).FirstOrDefault(u => u.Id == dto.Id)
                ?? throw new ArgumentInvalidException(nameof(dto), $"Invalid id {dto.Id}. You can't edit this user.");

            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 = dbContext.Users.FirstOrDefault(u => u.Id == dto.Id)
                ?? throw new ArgumentInvalidException(nameof(dto), $"Invalid id {dto.Id}. You can't edit this user.");
            
            entity.Id = dto.Id;
            entity.Login = dto.Login;
            entity.Name = dto.Name;
            entity.Email = dto.Email;
            entity.Phone = dto.Phone;
            entity.Surname = dto.Surname;
            entity.Patronymic = dto.Patronymic;
            entity.Position = dto.Position;
            entity.IdCompany = dto.IdCompany;
            entity.IdState = dto.IdState;
            await dbContext.SaveChangesAsync(token);
            DropCacheUsers();
            return entity.Id;            
        }

        public async Task<int> DeleteAsync(int id, CancellationToken token)
        {
            var user = (await GetCacheUserAsync(token)).FirstOrDefault(u => u.Id == id)
                ?? throw new ArgumentInvalidException(nameof(id), $"Invalid id {id}. You can't edit this user.");
            
            var query = dbContext
                .Users
                .Where(u => u.Id == id);
            if (query.Any())
            {
                var result = dbContext.Users.Remove(query.First());
                await dbContext.SaveChangesAsync(token);
                DropCacheUsers();
                return result.Entity.Id;
            }
            throw new ArgumentInvalidException
                    (nameof(id), $"Invalid id {id}. You can't edit this user.");
        }

        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)
        { 
            var userRoles = GetRolesByIdUser(idUser, 7)
                .Select(r => r.Caption)
                .Distinct();
            if (userRoles.Any())
                return userRoles;
            return Enumerable.Empty<string>();
        }

        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(nameof(login), $"Login {login} is busy by {existingUserDto.MakeDisplayName()}, id{existingUserDto.Id}");
        }
        
        private IEnumerable<RelationUserUserRole> GetCachRelationUserUserRoleCacheTag()
        {
            var cache = memoryCache.GetOrCreate(relationUserUserRoleCacheTag, cacheEntry =>
            {
                cacheEntry.AbsoluteExpirationRelativeToNow = cacheObsolence;
                cacheEntry.SlidingExpiration = cacheObsolence;
                var query = dbContext.RelationUserUserRoles
                        .Include(r => r.UserRole)
                        .Include(r => r.User);
                var entities = query.ToArray();
                return entities;
            });
            return cache!;
        }

        private void DropCacheRelationUserUserRoleCacheTag()
        { 
            memoryCache.Remove(relationUserUserRoleCacheTag);
        }

        private async Task UpdateRolesCacheForUserAsync(int idUser, IEnumerable<UserRoleDto> roleDtos, CancellationToken token)
        {
            var relations = dbContext.RelationUserUserRoles.Where(r => r.IdUser == idUser);
            dbContext.RelationUserUserRoles.RemoveRange(relations);
            var entityRoles = roleDtos.Select(role => new RelationUserUserRole
            {
                IdUser = idUser,
                IdUserRole = role.Id
            });
            dbContext.RelationUserUserRoles.AddRange(entityRoles);
            await dbContext.SaveChangesAsync(token);
            DropCacheRelationUserUserRoleCacheTag();
        }

        private void DropCacheUsers()
            => memoryCache.Remove(userCacheTag);

        private IEnumerable<User> GetCacheUser()
        {
            var cache = memoryCache.GetOrCreate(userCacheTag, cacheEntry =>
            {
                cacheEntry.AbsoluteExpirationRelativeToNow = cacheObsolence;
                cacheEntry.SlidingExpiration = cacheObsolence;
                var query = dbContext.Users
                        .Include(r => r.Company)
                        .Include(r => r.RelationUsersUserRoles);
                var entities = query.ToArray();
                return entities;
            });
            return cache!;
        }

        private Task<IEnumerable<User>> GetCacheUserAsync(CancellationToken token)
        {
            var cache = memoryCache.GetOrCreateAsync(userCacheTag, async (cacheEntry) =>
            {
                cacheEntry.AbsoluteExpirationRelativeToNow = cacheObsolence;
                cacheEntry.SlidingExpiration = cacheObsolence;
                var query = dbContext.Users
                        .Include(r => r.Company)
                        .Include(r => r.RelationUsersUserRoles);
                var entities = await query.ToArrayAsync(token);
                return entities.AsEnumerable();
            });
            return cache!;
        }

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

}