using System.Diagnostics;
using System.Text.Json;
using Microsoft.Extensions.Options;
using StackExchange.Redis;
using Tiku.Application.Security;
using Tiku.Domain.Tenancy;

namespace Tiku.Infrastructure.Security;

internal sealed class RedisAuthorizationCache(
    IConnectionMultiplexer connection,
    IOptions<AuthorizationCacheOptions> options,
    string environmentName) : IAccessSecurityCache, IAuthorizationSnapshotCache
{
    private const string SetVersionScript = """
                                            local current = redis.call('GET', KEYS[1])
                                            if current then
                                                local decoded = cjson.decode(current)
                                                if tonumber(decoded.version) > tonumber(ARGV[1]) then return 0 end
                                            end
                                            redis.call('SET', KEYS[1], ARGV[2], 'PX', ARGV[3])
                                            return 1
                                            """;

    private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
    private readonly AuthorizationCacheOptions options = options.Value;
    private readonly string prefix = $"tiku:{Normalize(environmentName)}";

    public bool IsConfigured => true;

    public async Task<AccessSecurityCacheState?> GetAsync(
        AccessSecurityCacheLookup lookup,
        CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        var keys = lookup.Realm == AuthRealm.Tenant
            ? new[]
            {
                SessionKey(lookup.SessionId), UserKey(lookup.UserId), VersionKey(lookup.Realm, lookup.TenantId),
                TenantKey(lookup.TenantId!.Value), MembershipKey(lookup.TenantId.Value, lookup.UserId)
            }
            : new[]
            {
                SessionKey(lookup.SessionId), UserKey(lookup.UserId), VersionKey(lookup.Realm, null),
                PlatformAccessKey(lookup.UserId)
            };
        var started = Stopwatch.GetTimestamp();
        var values = await connection.GetDatabase().StringGetAsync(keys).WaitAsync(cancellationToken);
        AuthorizationCacheTelemetry.RecordRedisDuration(Stopwatch.GetElapsedTime(started).TotalMilliseconds);
        var state = new AccessSecurityCacheState(
            Deserialize<CachedSessionSecurityState>(values[0]),
            Deserialize<CachedUserSecurityState>(values[1]),
            lookup.Realm == AuthRealm.Tenant ? Deserialize<CachedTenantSecurityState>(values[3]) : null,
            lookup.Realm == AuthRealm.Tenant ? Deserialize<CachedMembershipSecurityState>(values[4]) : null,
            lookup.Realm == AuthRealm.Platform ? Deserialize<CachedPlatformAccessState>(values[3]) : null,
            Deserialize<CachedAuthorizationVersion>(values[2]));
        AuthorizationCacheTelemetry.Read("redis_state", state.Complete);
        return state.Complete ? state : null;
    }

    public async Task SetAsync(AccessSecurityCacheState state, CancellationToken cancellationToken = default)
    {
        var ttl = StateTtl();
        var database = connection.GetDatabase();
        var writes = new List<Task>();
        Add(writes, database, state.Session is null ? default : SessionKey(state.Session.SessionId), state.Session,
            ttl);
        Add(writes, database, state.User is null ? default : UserKey(state.User.UserId), state.User, ttl);
        Add(writes, database, state.Tenant is null ? default : TenantKey(state.Tenant.TenantId), state.Tenant, ttl);
        Add(writes, database,
            state.Membership is null ? default : MembershipKey(state.Membership.TenantId, state.Membership.UserId),
            state.Membership, ttl);
        Add(writes, database, state.PlatformAccess is null ? default : PlatformAccessKey(state.PlatformAccess.UserId),
            state.PlatformAccess, ttl);
        await Task.WhenAll(writes).WaitAsync(cancellationToken);
        if (state.AuthorizationVersion is { } version)
            await SetAuthorizationVersionAsync(
                version.Realm, version.TenantId, version.Version, cancellationToken);
    }

    public Task InvalidateSessionAsync(Guid sessionId, CancellationToken cancellationToken = default)
    {
        return DeleteAsync(SessionKey(sessionId), cancellationToken);
    }

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

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

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

    public async Task SetAuthorizationVersionAsync(
        AuthRealm realm, Guid? tenantId, long version, CancellationToken cancellationToken = default)
    {
        var value = new CachedAuthorizationVersion(realm, tenantId, version);
        var ttl = StateTtl();
        await connection.GetDatabase().ScriptEvaluateAsync(
                SetVersionScript,
                [VersionKey(realm, tenantId)],
                [version, JsonSerializer.Serialize(value, SerializerOptions), (long)ttl.TotalMilliseconds])
            .WaitAsync(cancellationToken);
    }

    async Task<CachedAuthorizationSnapshot?> IAuthorizationSnapshotCache.GetAsync(
        AuthRealm realm, Guid? tenantId, Guid userId, CancellationToken cancellationToken)
    {
        var value = await connection.GetDatabase().StringGetAsync(SnapshotKey(realm, tenantId, userId))
            .WaitAsync(cancellationToken);
        return Deserialize<CachedAuthorizationSnapshot>(value);
    }

    Task IAuthorizationSnapshotCache.SetAsync(
        AuthRealm realm,
        Guid? tenantId,
        Guid userId,
        CachedAuthorizationSnapshot snapshot,
        CancellationToken cancellationToken)
    {
        return SetValueAsync(
            SnapshotKey(realm, tenantId, userId),
            snapshot,
            TimeSpan.FromSeconds(Math.Max(1, options.DistributedSnapshotSeconds)),
            cancellationToken);
    }

    private async Task DeleteAsync(RedisKey key, CancellationToken cancellationToken)
    {
        await connection.GetDatabase().KeyDeleteAsync(key).WaitAsync(cancellationToken);
    }

    private async Task SetValueAsync<T>(RedisKey key, T value, TimeSpan ttl, CancellationToken cancellationToken)
    {
        await connection.GetDatabase().StringSetAsync(key, JsonSerializer.Serialize(value, SerializerOptions), ttl)
            .WaitAsync(cancellationToken);
    }

    private static void Add<T>(List<Task> writes, IDatabase database, RedisKey key, T? value, TimeSpan ttl)
    {
        if (value is not null)
            writes.Add(database.StringSetAsync(key, JsonSerializer.Serialize(value, SerializerOptions), ttl));
    }

    private TimeSpan StateTtl()
    {
        var seconds = Math.Max(1, options.DistributedStateSeconds);
        var jitter = Math.Clamp(options.JitterPercent, 0, 50);
        return TimeSpan.FromSeconds(seconds * (1 + Random.Shared.Next(-jitter, jitter + 1) / 100d));
    }

    private static T? Deserialize<T>(RedisValue value)
    {
        return value.IsNullOrEmpty
            ? default
            : JsonSerializer.Deserialize<T>(value.ToString(), SerializerOptions);
    }

    private RedisKey SessionKey(Guid id)
    {
        return $"{prefix}:auth:session:v1:{id:N}";
    }

    private RedisKey UserKey(Guid id)
    {
        return $"{prefix}:auth:user:v1:{id:N}";
    }

    private RedisKey TenantKey(Guid id)
    {
        return $"{prefix}:auth:tenant:v1:{id:N}";
    }

    private RedisKey MembershipKey(Guid tenantId, Guid userId)
    {
        return $"{prefix}:auth:membership:v1:{tenantId:N}:{userId:N}";
    }

    private RedisKey PlatformAccessKey(Guid userId)
    {
        return $"{prefix}:auth:platform-access:v1:{userId:N}";
    }

    private RedisKey VersionKey(AuthRealm realm, Guid? tenantId)
    {
        return
            $"{prefix}:authz:version:v1:{realm.ToString().ToLowerInvariant()}:{tenantId?.ToString("N") ?? "platform"}";
    }

    private RedisKey SnapshotKey(AuthRealm realm, Guid? tenantId, Guid userId)
    {
        return
            $"{prefix}:authz:snapshot:v1:{realm.ToString().ToLowerInvariant()}:{tenantId?.ToString("N") ?? "platform"}:{userId:N}";
    }

    private static string Normalize(string value)
    {
        return new string(value.Trim().ToLowerInvariant()
            .Select(character => char.IsLetterOrDigit(character) || character is '-' or '_' ? character : '-')
            .ToArray());
    }
}

internal sealed class NullAuthorizationCache : IAccessSecurityCache, IAuthorizationSnapshotCache
{
    public bool IsConfigured => false;

    public Task<AccessSecurityCacheState?> GetAsync(AccessSecurityCacheLookup lookup,
        CancellationToken cancellationToken = default)
    {
        return Task.FromResult<AccessSecurityCacheState?>(null);
    }

    public Task SetAsync(AccessSecurityCacheState state, CancellationToken cancellationToken = default)
    {
        return Task.CompletedTask;
    }

    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 SetAuthorizationVersionAsync(AuthRealm realm, Guid? tenantId, long version,
        CancellationToken cancellationToken = default)
    {
        return Task.CompletedTask;
    }

    public Task<CachedAuthorizationSnapshot?> GetAsync(AuthRealm realm, Guid? tenantId, Guid userId,
        CancellationToken cancellationToken = default)
    {
        return Task.FromResult<CachedAuthorizationSnapshot?>(null);
    }

    public Task SetAsync(AuthRealm realm, Guid? tenantId, Guid userId, CachedAuthorizationSnapshot snapshot,
        CancellationToken cancellationToken = default)
    {
        return Task.CompletedTask;
    }
}