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

namespace Tiku.Infrastructure.Security;

internal sealed class AuthorizationStateInvalidator(
    TikuDbContext dbContext,
    IAccessSecurityCache cache) : IAuthorizationStateInvalidator
{
    public Task InvalidateSessionAsync(Guid sessionId, CancellationToken cancellationToken = default)
    {
        return ExecuteAsync("session", null, null, sessionId, null, null,
            token => cache.InvalidateSessionAsync(sessionId, token), cancellationToken);
    }

    public Task InvalidateUserAsync(Guid userId, CancellationToken cancellationToken = default)
    {
        return ExecuteAsync("user", null, userId, null, null, null,
            token => cache.InvalidateUserAsync(userId, token), cancellationToken);
    }

    public Task InvalidateTenantAsync(Guid tenantId, CancellationToken cancellationToken = default)
    {
        return ExecuteAsync("tenant", tenantId, null, null, AuthRealm.Tenant, null,
            token => cache.InvalidateTenantAsync(tenantId, token), cancellationToken);
    }

    public Task InvalidateMembershipAsync(Guid tenantId, Guid userId, CancellationToken cancellationToken = default)
    {
        return ExecuteAsync("membership", tenantId, userId, null, AuthRealm.Tenant, null,
            token => cache.InvalidateMembershipAsync(tenantId, userId, token), cancellationToken);
    }

    public async Task<long> BumpScopeAsync(AuthRealm realm, Guid? tenantId,
        CancellationToken cancellationToken = default)
    {
        var updated = await dbContext.AuthorizationScopeVersions
            .Where(item => item.Realm == realm && item.TenantId == tenantId)
            .ExecuteUpdateAsync(setters => setters
                .SetProperty(item => item.Version, item => item.Version + 1)
                .SetProperty(item => item.UpdatedAt, DateTimeOffset.UtcNow), cancellationToken);
        if (updated == 0)
        {
            dbContext.AuthorizationScopeVersions.Add(new AuthorizationScopeVersion
            {
                Realm = realm,
                TenantId = tenantId,
                Version = 2
            });
            await dbContext.SaveChangesAsync(cancellationToken);
        }

        var version = await dbContext.AuthorizationScopeVersions.AsNoTracking()
            .Where(item => item.Realm == realm && item.TenantId == tenantId)
            .Select(item => item.Version)
            .SingleAsync(cancellationToken);
        await ExecuteAsync("scope", tenantId, null, null, realm, version,
            token => cache.SetAuthorizationVersionAsync(realm, tenantId, version, token), cancellationToken);
        return version;
    }

    private async Task ExecuteAsync(
        string targetType, Guid? tenantId, Guid? userId, Guid? sessionId,
        AuthRealm? realm, long? version, Func<CancellationToken, Task> operation,
        CancellationToken cancellationToken)
    {
        if (!cache.IsConfigured) return;
        var invalidation = new AuthorizationCacheInvalidation
        {
            TargetType = targetType,
            TenantId = tenantId,
            UserId = userId,
            SessionId = sessionId,
            Realm = realm,
            Version = version
        };
        dbContext.AuthorizationCacheInvalidations.Add(invalidation);
        await dbContext.SaveChangesAsync(cancellationToken);
        try
        {
            await operation(cancellationToken);
            invalidation.ProcessedAt = DateTimeOffset.UtcNow;
            invalidation.AttemptCount++;
            await dbContext.SaveChangesAsync(cancellationToken);
            AuthorizationCacheTelemetry.Invalidated(targetType, true);
        }
        catch (Exception exception) when (exception is not OperationCanceledException)
        {
            invalidation.AttemptCount++;
            invalidation.LastError = exception.Message;
            await dbContext.SaveChangesAsync(CancellationToken.None);
            AuthorizationCacheTelemetry.Invalidated(targetType, false);
            if (dbContext.Database.CurrentTransaction is not null) return;
            throw new AuthorizationSecurityUnavailableException(exception);
        }
    }
}

internal sealed class NullAuthorizationStateInvalidator : IAuthorizationStateInvalidator
{
    public Task InvalidateSessionAsync(Guid sessionId, CancellationToken cancellationToken = default)
    {
        return Task.CompletedTask;
    }

    public Task InvalidateUserAsync(Guid userId, CancellationToken cancellationToken = default)
    {
        return Task.CompletedTask;
    }

    public Task InvalidateTenantAsync(Guid tenantId, CancellationToken cancellationToken = default)
    {
        return Task.CompletedTask;
    }

    public Task InvalidateMembershipAsync(Guid tenantId, Guid userId, CancellationToken cancellationToken = default)
    {
        return Task.CompletedTask;
    }

    public Task<long> BumpScopeAsync(AuthRealm realm, Guid? tenantId, CancellationToken cancellationToken = default)
    {
        return Task.FromResult(1L);
    }
}