using System.Security.Claims;
using MemberCenter.Application.Abstractions;
using MemberCenter.Application.Constants;
using MemberCenter.Domain.Entities;
using MemberCenter.Infrastructure.Identity;
using MemberCenter.Infrastructure.Persistence;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

namespace MemberCenter.Infrastructure.Services;

public sealed class AdminPermissionService : IAdminPermissionChecker, IAdminPermissionSeeder
{
    private readonly MemberCenterDbContext _dbContext;
    private readonly RoleManager<ApplicationRole> _roleManager;
    private Task<HashSet<string>>? _effectivePermissions;

    public AdminPermissionService(MemberCenterDbContext dbContext, RoleManager<ApplicationRole> roleManager)
    {
        _dbContext = dbContext;
        _roleManager = roleManager;
    }

    public async Task<bool> HasPermissionAsync(
        ClaimsPrincipal user,
        string permissionKey,
        CancellationToken cancellationToken = default)
    {
        if (user.Identity?.IsAuthenticated != true)
        {
            return false;
        }

        if (user.IsInRole(AdminPermissions.SuperuserRole))
        {
            return true;
        }

        if (user.IsInRole(AdminPermissions.TenantManagerRole))
        {
            return AdminPermissions.TenantManagerPermissions.Contains(permissionKey);
        }

        var roleNames = user.FindAll(ClaimTypes.Role)
            .Select(claim => claim.Value)
            .Where(value => !string.IsNullOrWhiteSpace(value))
            .Select(value => value.ToUpperInvariant())
            .Distinct(StringComparer.Ordinal)
            .ToList();

        if (roleNames.Count == 0)
        {
            return false;
        }

        _effectivePermissions ??= LoadEffectivePermissionsAsync(roleNames, cancellationToken);
        var effectivePermissions = await _effectivePermissions;
        return effectivePermissions.Contains(permissionKey);
    }

    private async Task<HashSet<string>> LoadEffectivePermissionsAsync(
        IReadOnlyCollection<string> normalizedRoleNames,
        CancellationToken cancellationToken)
    {
        var permissions = await _dbContext.AdminRolePermissions
            .AsNoTracking()
            .Where(rolePermission => rolePermission.Permission != null && rolePermission.Permission.IsEnabled)
            .Join(
                _dbContext.Roles.AsNoTracking(),
                rolePermission => rolePermission.RoleId,
                role => role.Id,
                (rolePermission, role) => new { rolePermission.Permission!.Key, role.NormalizedName })
            .Where(item => item.NormalizedName != null && normalizedRoleNames.Contains(item.NormalizedName))
            .Select(item => item.Key)
            .Distinct()
            .ToListAsync(cancellationToken);

        return permissions.ToHashSet(StringComparer.Ordinal);
    }

    public async Task EnsureDefaultsAsync(CancellationToken cancellationToken = default)
    {
        var permissionsByKey = await _dbContext.AdminPermissions
            .ToDictionaryAsync(permission => permission.Key, StringComparer.Ordinal, cancellationToken);

        foreach (var definition in AdminPermissions.All)
        {
            if (!permissionsByKey.TryGetValue(definition.Key, out var permission))
            {
                permission = new AdminPermission
                {
                    Id = Guid.NewGuid(),
                    Key = definition.Key
                };
                _dbContext.AdminPermissions.Add(permission);
                permissionsByKey[definition.Key] = permission;
            }

            permission.Name = definition.Name;
            permission.Description = definition.Description;
            permission.IsEnabled = true;
            permission.UpdatedAt = DateTimeOffset.UtcNow;
        }

        await EnsureRoleExistsAsync(AdminPermissions.AdminRole);
        await EnsureRoleExistsAsync(AdminPermissions.SuperuserRole);
        await EnsureRoleExistsAsync(AdminPermissions.TenantManagerRole);

        await _dbContext.SaveChangesAsync(cancellationToken);

        var adminRole = await _roleManager.FindByNameAsync(AdminPermissions.AdminRole)
            ?? throw new InvalidOperationException("Admin role was not created.");

        var currentPermissionIds = (await _dbContext.AdminRolePermissions
            .Where(rolePermission => rolePermission.RoleId == adminRole.Id)
            .Select(rolePermission => rolePermission.PermissionId)
            .ToListAsync(cancellationToken))
            .ToHashSet();

        foreach (var definition in AdminPermissions.All.Where(permission => !permission.SuperuserOnly))
        {
            var permission = permissionsByKey[definition.Key];
            if (currentPermissionIds.Contains(permission.Id))
            {
                continue;
            }

            _dbContext.AdminRolePermissions.Add(new AdminRolePermission
            {
                RoleId = adminRole.Id,
                PermissionId = permission.Id
            });
        }

        await _dbContext.SaveChangesAsync(cancellationToken);
    }

    private async Task EnsureRoleExistsAsync(string roleName)
    {
        if (await _roleManager.RoleExistsAsync(roleName))
        {
            return;
        }

        var result = await _roleManager.CreateAsync(new ApplicationRole
        {
            Id = Guid.NewGuid(),
            Name = roleName,
            NormalizedName = roleName.ToUpperInvariant()
        });

        if (!result.Succeeded)
        {
            throw new InvalidOperationException(string.Join("; ", result.Errors.Select(error => error.Description)));
        }
    }
}
