using System.Collections.Concurrent;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Tiku.Application.Auth;
using Tiku.Application.Security;
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,
    IMemoryCache memoryCache,
    IAuthorizationSnapshotCache snapshotCache,
    IOptions<AuthorizationCacheOptions> cacheOptions) : ICurrentAccessContext
{
    private static readonly ConcurrentDictionary<string, Lazy<Task<CurrentAccessSnapshot>>> SnapshotFlights = new();
    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 && cacheOptions.Value.Mode == AuthorizationCacheMode.Active)
            return await LoadCachedSnapshotAsync(
                userId,
                tenantContext.TenantId,
                validatedSession!.Realm,
                validatedSession.AuthorizationVersion,
                cancellationToken);
        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 async Task<CurrentAccessSnapshot> LoadCachedSnapshotAsync(
        Guid userId, Guid? tenantId, AuthRealm realm, long version, CancellationToken cancellationToken)
    {
        var localKey =
            $"authorization-snapshot:v1:{realm}:{tenantId?.ToString("N") ?? "platform"}:{userId:N}:{version}";
        if (memoryCache.TryGetValue<CurrentAccessSnapshot>(localKey, out var local) && local is not null)
        {
            AuthorizationCacheTelemetry.Read("l1_snapshot", true);
            return local;
        }

        AuthorizationCacheTelemetry.Read("l1_snapshot", false);

        try
        {
            var distributed = await snapshotCache.GetAsync(realm, tenantId, userId, cancellationToken);
            if (distributed is not null && distributed.Version == version)
            {
                AuthorizationCacheTelemetry.Read("redis_snapshot", true);
                memoryCache.Set(localKey, distributed.Snapshot,
                    TimeSpan.FromSeconds(cacheOptions.Value.LocalSnapshotSeconds));
                return distributed.Snapshot;
            }

            if (distributed is not null) AuthorizationCacheTelemetry.VersionMismatch();
        }
        catch (Exception exception) when (exception is not OperationCanceledException)
        {
            // A confirmed session may safely fall back to the authorization source of truth.
        }

        AuthorizationCacheTelemetry.Read("redis_snapshot", false);
        AuthorizationCacheTelemetry.PostgresFallback();
        var flight = SnapshotFlights.GetOrAdd(localKey, _ => new Lazy<Task<CurrentAccessSnapshot>>(
            () => LoadPermissionSnapshotAsync(userId, tenantId, CancellationToken.None),
            LazyThreadSafetyMode.ExecutionAndPublication));
        CurrentAccessSnapshot snapshot;
        try
        {
            snapshot = await flight.Value;
        }
        finally
        {
            SnapshotFlights.TryRemove(new KeyValuePair<string, Lazy<Task<CurrentAccessSnapshot>>>(localKey, flight));
        }

        memoryCache.Set(localKey, snapshot, TimeSpan.FromSeconds(cacheOptions.Value.LocalSnapshotSeconds));
        try
        {
            await snapshotCache.SetAsync(realm, tenantId, userId,
                new CachedAuthorizationSnapshot(version, snapshot), cancellationToken);
        }
        catch (Exception exception) when (exception is not OperationCanceledException)
        {
            // PostgreSQL remains authoritative; a later request can refill Redis.
        }

        return snapshot;
    }

    private async Task<CurrentAccessSnapshot> LoadPermissionSnapshotAsync(
        Guid userId, Guid? tenantId, CancellationToken cancellationToken)
    {
        if (tenantId is null)
            return new CurrentAccessSnapshot(
                userId, null, true, false, new HashSet<string>(StringComparer.Ordinal),
                await LoadPlatformPermissionsAsync(userId, cancellationToken), CurrentDataScope.Self);

        var roles = 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 = roles.Select(role => role.Id).ToArray();
        var permissions = 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, permissions, new HashSet<string>(StringComparer.Ordinal),
            CurrentDataScope.Merge(roles.Select(role => role.DataScope)));
    }

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