using AsbCloudApp.Data;
using AsbCloudApp.Services;
using AsbCloudDb.Model;
using AsbCloudInfrastructure.Services.Cache;
using Mapster;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace AsbCloudInfrastructure.Services
{
    public class UserService :  IUserService
    {
        private readonly CacheTable<User> cacheUsers;
        private readonly CacheTable<RelationUserUserRole> cacheRelationUserToRoles;
        public ISet<string> Includes { get; } = new SortedSet<string>();
        public IUserRoleService RoleService { get; }

        private static readonly TypeAdapterConfig userTypeAdapterConfig = TypeAdapterConfig<UserExtendedDto, User>
            .NewConfig()
            .Ignore(dst => dst.Company,
                dst => dst.FileMarks,
                dst => dst.Files,
                dst => dst.RelationUsersUserRoles)
            .Config;

        public UserService(IAsbCloudDbContext context, CacheDb cacheDb, IUserRoleService roleService)
        {
            var db = (AsbCloudDbContext)context;
            cacheUsers = cacheDb.GetCachedTable<User>(
                db,
                new[] { 
                    nameof(User.RelationUsersUserRoles),
                    nameof(User.Company),
                });
            cacheRelationUserToRoles = cacheDb.GetCachedTable<RelationUserUserRole>(
                db,
                new[] {
                    nameof(RelationUserUserRole.User),
                    nameof(RelationUserUserRole.UserRole),
                });
            RoleService = roleService;
        }

        public async Task<int> InsertAsync(UserExtendedDto dto, CancellationToken token = default)
        {
            var entity = Convert(dto);
            await AssertLoginAsync(dto.Login, token);
            var userRoles = await RoleService.GetByNamesAsync(dto.RoleNames, token).ConfigureAwait(false);
            var updatedEntity = await cacheUsers.InsertAsync(entity, token).ConfigureAwait(false);
            if (userRoles?.Any() == true)
                await UpdateRolesCacheForUserAsync(updatedEntity.Id, userRoles, token);
            return updatedEntity?.Id ?? 0;
        }

        private async Task AssertLoginAsync(string login, CancellationToken token = default)
        {
            var existingUser = await cacheUsers.FirstOrDefaultAsync(u => u.Login.ToLower() == login.ToLower(), token);
            if (existingUser is not null)
                throw new ArgumentException($"Login {login} is busy by {existingUser.MakeDisplayName()}, id{existingUser.Id}", nameof(login));
        }

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

        public async Task<IEnumerable<UserExtendedDto>> GetAllAsync(CancellationToken token = default)
        {
            var entities = (await cacheUsers.WhereAsync(token).ConfigureAwait(false))
                .ToList();
            if (entities.Count == 0)
                return null;
            var dtos = entities.Select(Convert).ToList();
            for (var i = 0; i < dtos.Count; i++)
                dtos[i].RoleNames = GetRolesNamesByIdUser(dtos[i].Id);
            return dtos;
        }

        public async Task<UserExtendedDto> GetAsync(int id, CancellationToken token = default)
        {
            var entity = await cacheUsers.FirstOrDefaultAsync(u=>u.Id == id, token).ConfigureAwait(false);
            var dto = Convert(entity);
            dto.RoleNames = GetRolesNamesByIdUser(dto.Id);
            return dto;
        }

        public async Task<int> UpdateAsync(int id, UserExtendedDto dto, CancellationToken token = default)
        {
            var oldUser = await cacheUsers.FirstOrDefaultAsync(u=>u.Id == id, token);
            if(oldUser.Login != dto.Login)
                await AssertLoginAsync(dto.Login, token);

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

            var entity = Convert(dto);
            if (dto.Id == 0)
                entity.Id = id;
            else if (dto.Id != id)
                throw new ArgumentException($"Invalid userDto.id it mast be 0 or {id}", nameof(dto));

            var result = await cacheUsers.UpsertAsync(entity, token)
                .ConfigureAwait(false);
            return result;
        }

        public Task<int> DeleteAsync(int id, CancellationToken token = default)
        => cacheUsers.RemoveAsync(r => r.Id == id, token);

        public Task<int> DeleteAsync(IEnumerable<int> ids, CancellationToken token = default)
        => cacheUsers.RemoveAsync(r => ids.Contains(r.Id), token);

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

        public IEnumerable<UserRoleDto> GetRolesByIdUser(int idUser)
        {
            var roles = cacheRelationUserToRoles.Where(r => r.IdUser == idUser);
            if (roles?.Any() != true)
                return null;
            return roles.SelectMany(r => RoleService.GetNestedById(r.IdUserRole));
        }

        public IEnumerable<PermissionDto> GetNestedPermissions(int idUser)
        {
            var roles = GetRolesByIdUser(idUser);
            if(roles?.Any() != true)
                return null;
            var permissions = roles
                .Where(r => r.Permissions is not null)
                .SelectMany(r => r.Permissions);

            return permissions;
        }

        private async Task UpdateRolesCacheForUserAsync(int idUser, IEnumerable<UserRoleDto> newRoles, CancellationToken token)
        {
            await cacheRelationUserToRoles.RemoveAsync(r => r.IdUser == idUser, token)
                    .ConfigureAwait(false);

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

        public bool HasAnyRoleOf(int idUser, IEnumerable<string> roleNames)
        {
            if(!roleNames.Any())
                return true;
            var userRoleNames = GetRolesNamesByIdUser(idUser);
            foreach (var roleName in userRoleNames)
                if (roleNames.Contains(roleName))
                    return true;
                return false;
        }

        public bool HasAnyRoleOf(int idUser, IEnumerable<int> roleIds)
        {
            if (!roleIds.Any())
                return true;
            var userRoles = GetRolesByIdUser(idUser);
            foreach (var role in userRoles)
                if (roleIds.Contains(role.Id))
                    return true;
            return false;
        }

        public bool HasPermission(int idUser, string permissionName)
        {
            var relationsToRoles = cacheRelationUserToRoles.Where(r=>r.IdUser == idUser);
            if (relationsToRoles is null)
                return false;

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

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

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