using Microsoft.EntityFrameworkCore;
using Tiku.Application.Security;
using Tiku.Application.Auth;
using Tiku.Domain.Identity;
using Tiku.Domain.Operations;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;

namespace Tiku.Infrastructure.Security;

internal sealed class CurrentAccessContext(
    ICurrentUser currentUser,
    ITenantContext tenantContext,
    IRequestSecurityState requestSecurityState,
    TikuDbContext dbContext) : ICurrentAccessContext
{
    private Task<CurrentAccessSnapshot>? snapshotTask;

    public Task<CurrentAccessSnapshot> GetAsync(CancellationToken cancellationToken = default)
    {
        // The context is scoped to one request. Do not allow an aborted authorization
        // check to poison the cached access snapshot used later in that request.
        return snapshotTask ??= LoadAsync(CancellationToken.None);
    }

    private async Task<CurrentAccessSnapshot> LoadAsync(CancellationToken cancellationToken)
    {
        if (!currentUser.IsAuthenticated || currentUser.UserId is not { } userId)
        {
            return Empty();
        }

        var validatedSession = requestSecurityState.ValidatedSession;
        var isValidated = validatedSession is not null &&
                          validatedSession.UserId == userId &&
                          validatedSession.TenantId == tenantContext.TenantId;
        if (!isValidated)
        {
            var isUserActive = await dbContext.Users.AsNoTracking()
                .AnyAsync(user => user.Id == userId && user.Status == UserStatus.Active, cancellationToken);
            if (!isUserActive)
            {
                return new CurrentAccessSnapshot(
                    userId,
                    tenantContext.TenantId,
                    false,
                    false,
                    new HashSet<string>(StringComparer.Ordinal),
                    new HashSet<string>(StringComparer.Ordinal),
                    CurrentDataScope.Self);
            }
        }

        if (tenantContext.TenantId is not { } tenantId)
        {
            var platformPermissions = await LoadPlatformPermissionsAsync(userId, cancellationToken);
            return new CurrentAccessSnapshot(
                userId,
                null,
                true,
                false,
                new HashSet<string>(StringComparer.Ordinal),
                platformPermissions,
                CurrentDataScope.Self);
        }

        if (!isValidated)
        {
            var isTenantActive = await dbContext.Tenants.AsNoTracking()
                .AnyAsync(tenant => tenant.Id == tenantId && tenant.Status == TenantStatus.Active, cancellationToken);
            var isActiveMember = isTenantActive && await dbContext.TenantMemberships.AsNoTracking()
                .AnyAsync(
                    membership => membership.TenantId == tenantId &&
                                  membership.UserId == userId &&
                                  membership.Status == MembershipStatus.Active,
                    cancellationToken);
            if (!isActiveMember)
            {
                return new CurrentAccessSnapshot(
                    userId,
                    tenantId,
                    true,
                    false,
                    new HashSet<string>(StringComparer.Ordinal),
                    new HashSet<string>(StringComparer.Ordinal),
                    CurrentDataScope.Self);
            }
        }

        var tenantRoles = await (
                from userRole in dbContext.TenantBackendUserRoles.AsNoTracking()
                join role in dbContext.TenantBackendRoles.AsNoTracking() on userRole.RoleId equals role.Id
                where userRole.TenantId == tenantId &&
                      userRole.UserId == userId &&
                      role.Status == BackendRoleStatus.Active
                select new { role.Id, role.DataScope })
            .ToArrayAsync(cancellationToken);

        var roleIds = tenantRoles.Select(role => role.Id).ToArray();
        var tenantPermissions = roleIds.Length == 0
            ? new HashSet<string>(StringComparer.Ordinal)
            : (await (
                    from binding in dbContext.TenantBackendRolePermissions.AsNoTracking()
                    join permission in dbContext.BackendPermissions.AsNoTracking()
                        on binding.PermissionCode equals permission.Code
                    where binding.TenantId == tenantId &&
                          roleIds.Contains(binding.RoleId) &&
                          (permission.Area == BackendPermissionArea.Tenant || permission.Area == BackendPermissionArea.Both)
                    select binding.PermissionCode)
                .Distinct()
                .ToArrayAsync(cancellationToken))
                .ToHashSet(StringComparer.Ordinal);

        return new CurrentAccessSnapshot(
            userId,
            tenantId,
            true,
            true,
            tenantPermissions,
            new HashSet<string>(StringComparer.Ordinal),
            CurrentDataScope.Merge(tenantRoles.Select(role => role.DataScope)));
    }

    private async Task<HashSet<string>> LoadPlatformPermissionsAsync(Guid userId, CancellationToken cancellationToken)
    {
        return (await (
                from userRole in dbContext.PlatformBackendUserRoles.AsNoTracking()
                join role in dbContext.PlatformBackendRoles.AsNoTracking() on userRole.RoleId equals role.Id
                join binding in dbContext.PlatformBackendRolePermissions.AsNoTracking() on role.Id equals binding.RoleId
                join permission in dbContext.BackendPermissions.AsNoTracking()
                    on binding.PermissionCode equals permission.Code
                where userRole.UserId == userId &&
                      role.Status == BackendRoleStatus.Active &&
                      (permission.Area == BackendPermissionArea.Platform || permission.Area == BackendPermissionArea.Both)
                select binding.PermissionCode)
            .Distinct()
            .ToArrayAsync(cancellationToken))
            .ToHashSet(StringComparer.Ordinal);
    }

    private CurrentAccessSnapshot Empty()
    {
        return new CurrentAccessSnapshot(
            null,
            tenantContext.TenantId,
            false,
            false,
            new HashSet<string>(StringComparer.Ordinal),
            new HashSet<string>(StringComparer.Ordinal),
            CurrentDataScope.Self);
    }
}
