using e_suite.Utilities.Pagination;
using eSuite.Core.Miscellaneous;
using System.Linq.Expressions;
using e_suite.Database.Audit;
using e_suite.Database.Core.Tables.Domain;
using eSuite.Core.Security;
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using e_suite.API.Common;
using e_suite.API.Common.exceptions;
using e_suite.API.Common.models;
using e_suite.API.Common.repository;
using e_suite.Database.Core.Extensions;
using e_suite.Database.Core.Tables.UserManager;
using MockQueryable;

namespace e_suite.Modules.RoleManager;

public class RoleManager : IRoleManager
{
    private readonly IRoleManagerRepository _roleManagerRepository;
    private readonly IUserManagerRepository _userManagerRepository;
    private readonly IDomainRepository _domainRepository;

    public RoleManager(IRoleManagerRepository roleManagerRepository, IDomainRepository domainRepository, IUserManagerRepository userManagerRepository)
    {
        _roleManagerRepository = roleManagerRepository;
        _domainRepository = domainRepository;
        _userManagerRepository = userManagerRepository;
    }

    public async Task<IPaginatedData<ReadRole>> GetRoles(Paging paging, IGeneralIdRef domainId, CancellationToken cancellationToken)
    {
        var roles = _roleManagerRepository.GetRolesList();

        var domain = await _domainRepository.GetDomainById(domainId, cancellationToken);

        if (domain != null)
        {
            roles = roles.Where( role => role.Domain == domain);
        }

        var paginatedData = await PaginatedData.Paginate(roles, paging,
            KeySelector, cancellationToken);

        var paginatedResult = new PaginatedData<ReadRole>
        {
            Count = paginatedData.Count,
            Page = paginatedData.Page,
            PageSize = paginatedData.PageSize,
            Data = paginatedData.Data.Select(ToReadRole)

        };

        return paginatedResult;
    }

    private ReadRole ToReadRole(Role role)
    {
        return new ReadRole
        {
            Id = role.Id,
            Guid = role.Guid,
            Name = role.Name,
            CanDelete = role.CanDelete,
            IsAdministrator = role.IsAdministrator,
            IsSuperUser = role.IsSuperUser,
            DomainId = new GeneralIdRef
            {
                Id = role.Domain.Id,
                Guid = role.Domain.Guid
            },
            DomainName = role.Domain.Name
        };
    }
    private Expression<Func<Role, object>> KeySelector(string? sortKey)
    {
        return sortKey?.ToLowerInvariant() switch
        {
            "id" => x => x.Id,
            "guid" => x => x.Guid,
            "name" => x => x.Name,
            _ => x => x.Name
        };
    }

    public async Task<ReadRole> GetRole(IGeneralIdRef generalIdRef, CancellationToken cancellationToken)
    {
        var role = await _roleManagerRepository.GetRoleByIdAsync(generalIdRef, cancellationToken) ??
            throw new NotFoundException("Role not found");

        if (role.Deleted)
            throw new NotFoundException("Role not found");

        var readRole = ToReadRole(role);

        return readRole;
    }

    public async Task CreateRole(AuditUserDetails auditUserDetails, CreateRole role,
        CancellationToken cancellationToken)
    {
        await _roleManagerRepository.TransactionAsync(async () =>
        {
            var existingRole = role.Guid == null
                ? null
                : await _roleManagerRepository.GetRoleByIdAsync(new GeneralIdRef
                {
                    Guid = role.Guid
                }, cancellationToken);

            if (existingRole != null && !existingRole.Deleted)
                throw new ExistsException($"Role with guid '{role.Guid}' already exists");

            if (role.DomainId is null)
                throw new InvalidOperationException("DomainId cannot be null");

            var domain = await _domainRepository.GetDomainById(role.DomainId, cancellationToken) ??
                throw new NotFoundException("Unable to find domain");

            if (existingRole == null)
            {
                existingRole = await _roleManagerRepository.GetRoleByName(domain, role.Name, cancellationToken);
                if (existingRole != null && !existingRole.Deleted)
                    throw new ExistsException($"Role with name '{role.Name}' already exists");
            }

            existingRole ??= new Role
            {
                Guid = role.Guid ?? Guid.NewGuid()
            };
            
            existingRole.Name = role.Name;
            existingRole.CanDelete = role.CanDelete;
            existingRole.IsAdministrator = role.IsAdministrator;
            existingRole.IsSuperUser = role.IsSuperUser;
            existingRole.Domain = domain;

            if (existingRole.Deleted)
            {
                existingRole.Deleted = false;
                await _roleManagerRepository.EditRole(auditUserDetails, existingRole, cancellationToken);
            }
            else
            {
                await _roleManagerRepository.AddRole(auditUserDetails, existingRole, cancellationToken);
            }
        });
    }

    public async Task EditRole(AuditUserDetails auditUserDetails, EditRole editRole,
        CancellationToken cancellationToken)
    {
        await _roleManagerRepository.TransactionAsync(async () =>
        {
            if (editRole.GeneralIdRef is null)
                throw new InvalidOperationException("GeneralIdRef cannot be null");

            var existingRole =
                await _roleManagerRepository.GetRoleByIdAsync(editRole.GeneralIdRef, cancellationToken);

            if (existingRole == null || existingRole.Deleted)
                throw new NotFoundException("A role with this Id doesn't exist");

            var identicalNameSequence = await _roleManagerRepository.GetRoleByName(existingRole.Domain, editRole.Name, cancellationToken);

            if (identicalNameSequence != null && (identicalNameSequence.Id != editRole.GeneralIdRef.Id &&
                                                  identicalNameSequence.Guid != editRole.GeneralIdRef.Guid))
                throw new ExistsException($"Role with name '{editRole.Name}' already exists");

            existingRole.Name = editRole.Name;

            await _roleManagerRepository.EditRole(auditUserDetails, existingRole, cancellationToken);
        });
    }

    public async Task DeleteRole(AuditUserDetails auditUserDetails, IGeneralIdRef generalIdRef,
        CancellationToken cancellationToken)
    {
        await _roleManagerRepository.TransactionAsync(async () =>
        {
            var existingRole = await _roleManagerRepository.GetRoleByIdAsync(generalIdRef, cancellationToken);
            if (existingRole == null || existingRole.Deleted)
                throw new NotFoundException("A role with this Id does not exist");

            existingRole.Deleted = true;

            await _roleManagerRepository.EditRole(auditUserDetails, existingRole, cancellationToken);
        });
    }

    public async Task<IPaginatedData<RoleUser>> GetRoleUsers(Paging paging, GeneralIdRef roleId, CancellationToken cancellationToken)
    {
        var role = await _roleManagerRepository.GetRoleByIdAsync(roleId, cancellationToken);

        var roleUsers = _roleManagerRepository.GetUserRoles();

        if (role != null)
        {
            roleUsers = roleUsers.Where(roleUser => roleUser.Role == role);
        }

        var paginatedData = await PaginatedData.Paginate(roleUsers, paging,
            UserRoleKeySelector, cancellationToken);

        var paginatedResult = new PaginatedData<RoleUser>
        {
            Count = paginatedData.Count,
            Page = paginatedData.Page,
            PageSize = paginatedData.PageSize,
            Data = paginatedData.Data.Select(ToRoleUser)

        };

        return paginatedResult;
    }

    private RoleUser ToRoleUser(UserRole userRole)
    {
        return new RoleUser
        {
            Guid = userRole.User.Guid,
            Id = userRole.User.Id,
            DisplayName = userRole.User.DisplayName
        };
    }
    
    private Expression<Func<UserRole, object>> UserRoleKeySelector(string? sortKey)
    {
        return sortKey?.ToLowerInvariant() switch
        {
            "userid" => x => x.User.DisplayName,
            "displayname" => x => x.User.DisplayName,
            _ => x => x.Id
        };
    }

    public async Task AddRoleUser(AuditUserDetails auditUserDetails, UserRoleIds userRoleIds,
        CancellationToken cancellationToken)
    {
        await _roleManagerRepository.TransactionAsync(async () =>
        {
            var existingRole = await _roleManagerRepository.GetRoleByIdAsync(userRoleIds.RoleId, cancellationToken);

            if (existingRole == null || existingRole.Deleted)
                throw new NotFoundException("Role Not Found");

            var existingUser = await _userManagerRepository.GetUserByIdAsync(userRoleIds.UserId, cancellationToken);

            if (existingUser == null || !existingUser.Active)
                throw new NotFoundException("User not Found");

            await _roleManagerRepository.AddUserRole(auditUserDetails, existingRole, existingUser, cancellationToken);
        });
    }

    public async Task DeleteRoleUser(AuditUserDetails auditUserDetails, UserRoleIds userRoleIds,
        CancellationToken cancellationToken)
    {
        await _roleManagerRepository.TransactionAsync(async () =>
        {
            var existingRole = await _roleManagerRepository.GetRoleByIdAsync(userRoleIds.RoleId, cancellationToken);

            if (existingRole == null || existingRole.Deleted)
                throw new NotFoundException("Role Not Found");

            var existingUser = await _userManagerRepository.GetUserByIdAsync(userRoleIds.UserId, cancellationToken);

            if (existingUser == null || !existingUser.Active)
                throw new NotFoundException("User not Found");

            await _roleManagerRepository.DeleteUserRole(auditUserDetails, existingRole, existingUser, cancellationToken);
        });
    }

    public async Task<IPaginatedData<GetSecurityAccess>> GetAccessList(Paging paging, CancellationToken cancellationToken)
    {
        var accessList = EnumerateAccessList();

        var paginatedResult = await PaginatedData.Paginate(accessList, paging,
            AccessListKeySelector, cancellationToken);

        return paginatedResult;
    }
    
    private Expression<Func<GetSecurityAccess, object>> AccessListKeySelector(string? sortKey)
    {
        return sortKey?.ToLowerInvariant() switch
        {
            "securityaccess" => x => x.SecurityAccess,
            "name" => x => x.Name,
            "description" => x => x.Description,
            "groupname" => x => x.GroupName,
            _ => x => x.Name
        };
    }

    public async Task<IPaginatedData<GetRoleSecurityAccess>> GetRoleAccess(Paging paging, CancellationToken cancellationToken)
    {
        var accessList = _roleManagerRepository.GetAccessForRole();

        var paginatedData = await PaginatedData.Paginate(accessList, paging,
            RoleAccessKeySelector, cancellationToken);

        var paginatedResult = new PaginatedData<GetRoleSecurityAccess>
        {
            Count = paginatedData.Count,
            Page = paginatedData.Page,
            PageSize = paginatedData.PageSize,
            Data = paginatedData.Data.Select(
                value =>
                {
                    var accessKey = (SecurityAccess)value.AccessKey;
                    var displayAttribute = GetEnumAttribute<DisplayAttribute>(accessKey);

                    return new GetRoleSecurityAccess
                    {
                        RoleId = value.Role.ToGeneralIdRef()!,
                        SecurityAccess = accessKey,
                        Description = displayAttribute?.Description!,
                        GroupName = displayAttribute?.GroupName!,
                        Name = displayAttribute?.Name!
                    };
                }
            )

        };

        return paginatedResult;
    }

    public async Task AddRoleSecurityAccess(AuditUserDetails auditUserDetails, AddRoleSecurityAccess accessToAdd, CancellationToken cancellationToken)
    {
        var role = await _roleManagerRepository.GetRoleByIdAsync(accessToAdd.RoleId, cancellationToken) ??
            throw new NotFoundException("Role Not Found");

        var rollAccessToAdd = accessToAdd.SecurityAccess.Select(
            securityAccess => new RoleAccess
            {
                Role = role,
                RoleId = role.Id,
                AccessKey = (int)securityAccess
            }
        ).ToList();

        await _roleManagerRepository.AddSecurityAccess(auditUserDetails, rollAccessToAdd, cancellationToken);
    }

    public async Task DeleteRoleSecurityAccess(AuditUserDetails auditUserDetails,
        DeleteRoleSecurityAccess accessToRemove,
        CancellationToken cancellationToken
    )
    {
        var role = await _roleManagerRepository.GetRoleByIdAsync(accessToRemove.RoleId, cancellationToken) ??
            throw new NotFoundException("Role Not Found");

        var rollAccessToDelete = _roleManagerRepository.GetAccessForRole()
            .Where(x => x.RoleId == role.Id && accessToRemove.SecurityAccess.Contains( (SecurityAccess)x.AccessKey)).ToList();

        await _roleManagerRepository.DeleteSecurityAccess(auditUserDetails, rollAccessToDelete, cancellationToken);
    }

    private bool IsAdministratorInAnyDomain(User user)
    {
        var userRoles = _roleManagerRepository.GetUserRoles().Where(x => x.UserId == user.Id && x.Deleted == false).ToList();

        foreach (var role in userRoles)
        {
            if (role.Role.IsSuperUser) //Superusers have access for all domains
                return true;

            if (role.Role.IsAdministrator) //Administrators have access to all keys in their domain
                return true;
        }

        return false;
    }

    private bool IsAdministratorInDomain(long userId, long domainId)
    {
        var userRoles = _roleManagerRepository.GetUserRoles().Where(x => x.UserId == userId && x.Deleted == false).ToList();

        foreach (var role in userRoles)
        {
            if (role.Role.IsSuperUser) //Superusers have access for all domains
                return true;

            if (role.Role.IsAdministrator && role.Role.DomainId == domainId) //Administrators have access to all keys in their domain
                return true;
        }

        return false;
    }

    public async Task<bool> HasAnyAccess(long? userId, SecurityAccess accessKey)
    {
        //Next check to see if the user is a member of a role with access to the key.
        var generalIdRef = new GeneralIdRef
        {
            Id = userId
        };

        var user = await _userManagerRepository.GetUserByIdAsync(generalIdRef, CancellationToken.None) ??
                   throw new NotFoundException("User Not Found");

        if (!user.Active)
            return false;

        if (accessKey == SecurityAccess.Everyone)
            return true;

        var hasAccess = _roleManagerRepository.GetUserAccess().FirstOrDefault(x => x.UserId == user.Id && x.DomainId == user.DomainId && x.AccessKey == (int)accessKey);
        if (hasAccess != null)
            return true;

        return IsAdministratorInAnyDomain(user);
    }

    public async Task CheckHasDomainAccess(long userId, IGeneralIdRef? specificDomain, SecurityAccess accessKey, CancellationToken cancellationToken)
    {
        var domainToCheck = await GetDomainToCheck(userId, specificDomain, cancellationToken) ??
                            throw new NotFoundException("Domain Not Found");

        var hasAccess = _roleManagerRepository
            .GetUserAccess().FirstOrDefault(x => x.UserId == userId && x.DomainId == domainToCheck.Id && x.AccessKey == (int)accessKey);

        if (hasAccess != null)
            return;

        if (IsAdministratorInDomain(userId, domainToCheck.Id))
            return;

        throw new UnauthorizedAccessException();
    }

    private async Task<Domain?> GetDomainToCheck(long userId, IGeneralIdRef? specificDomain, CancellationToken cancellationToken)
    {
        Domain? domainToCheck;

        if (specificDomain != null)
            domainToCheck = await _domainRepository.GetDomainById(specificDomain, cancellationToken);
        else
        {
            var user = await _userManagerRepository.GetUserByIdAsync(new GeneralIdRef
            {
                Id = userId
            }, cancellationToken);

            domainToCheck = await _domainRepository.GetDomainById(user!.Domain!.ToGeneralIdRef()!, cancellationToken);
        }

        return domainToCheck;
    }
    
    private Expression<Func<RoleAccess, object>> RoleAccessKeySelector(string? sortKey)
    {
        return sortKey?.ToLowerInvariant() switch
        {
            "roleid" => x => x.RoleId,
            _ => x => x.AccessKey
        };
    }

    private static IQueryable<GetSecurityAccess> EnumerateAccessList()
    {
        var results = from value in Enum.GetValues<SecurityAccess>()
            let hiddenAttribute = GetEnumAttribute<HiddenAttribute>(value)
            where hiddenAttribute == null
            let obsoleteAttribute = GetEnumAttribute<ObsoleteAttribute>(value)
            where obsoleteAttribute == null
            let displayAttribute = GetEnumAttribute<DisplayAttribute>(value)
            where displayAttribute != null
            select new GetSecurityAccess
            {
                SecurityAccess = value,
                Description = displayAttribute?.Description ?? string.Empty,
                GroupName = displayAttribute?.GroupName ?? string.Empty,
                Name = displayAttribute?.Name ?? value.ToString()
            };

        return results.ToList().BuildMock();
    }

    public static T? GetEnumAttribute<T>(Enum value) where T : class
    {
        FieldInfo? fieldInfo = value.GetType().GetField(value.ToString());
        if (fieldInfo?.GetCustomAttributes(typeof(T), false) is T[] attributes && attributes.Length > 0)
        {
            return attributes.First();
        }

        return null;
    }

    public IEnumerable<GetSecurityAccess> GetMyUserAccess(User user)
    {
        var accessList = EnumerateAccessList();

        if (IsAdministratorInAnyDomain(user))
#pragma warning disable IDE0305
            return accessList.ToList();
#pragma warning restore IDE0305

        var userAccess = _roleManagerRepository.GetUserAccess().Where(x => x.UserId == user.Id && x.DomainId == user.DomainId)
            .Select(x => (SecurityAccess)x.AccessKey)
            .ToList();

        var filteredAccessList = accessList.Where(x => userAccess.Contains(x.SecurityAccess));
        return filteredAccessList;
    }
}
