using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Tiku.Application.Security;
using Tiku.Domain.Platform;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;

namespace Tiku.Infrastructure.Security;

internal sealed record TenantFeatureDefinition(string Code, bool IsCore);

internal sealed record TenantSubscriptionSnapshot(
    Guid Id,
    Guid BaseOfferingVersionId,
    TenantSaasSubscriptionStatus Status,
    DateTimeOffset StartsAt,
    DateTimeOffset CurrentPeriodEnd);

internal sealed record TenantFeatureAccessSnapshot(
    TenantStatus? TenantStatus,
    TenantSubscriptionSnapshot? Subscription,
    TenantFeatureDefinition[] Features,
    Dictionary<string, TenantFeatureOverrideMode> Overrides,
    HashSet<string> PurchasedFeatures)
{
    public FeatureAccessDecision Evaluate(string featureCode, FeatureAccessOperation operation)
    {
        var normalized = Normalize(featureCode);
        if (TenantStatus != Domain.Tenancy.TenantStatus.Active) return Denied(normalized, operation, "tenant_inactive");

        var feature = Features.SingleOrDefault(value => value.Code == normalized);
        if (feature is null) return Denied(normalized, operation, "feature_unavailable");

        if (feature.IsCore) return Allowed(normalized, operation);

        var hasOverride = Overrides.TryGetValue(normalized, out var overrideMode);
        if (hasOverride && overrideMode == TenantFeatureOverrideMode.Disabled)
            return Denied(normalized, operation, "feature_disabled");

        if (Subscription is null)
            return hasOverride && overrideMode == TenantFeatureOverrideMode.Enabled
                ? Allowed(normalized, operation)
                : Denied(normalized, operation, "subscription_missing");

        var now = DateTimeOffset.UtcNow;
        if (operation == FeatureAccessOperation.Write &&
            (Subscription.Status is not (TenantSaasSubscriptionStatus.Trial or TenantSaasSubscriptionStatus.Active) ||
             Subscription.StartsAt > now || Subscription.CurrentPeriodEnd <= now))
            return Denied(normalized, operation, "subscription_read_only");

        if (Subscription.Status == TenantSaasSubscriptionStatus.Suspended)
            return Denied(normalized, operation, "subscription_suspended");

        if (hasOverride && overrideMode == TenantFeatureOverrideMode.Enabled) return Allowed(normalized, operation);

        return PurchasedFeatures.Contains(normalized)
            ? Allowed(normalized, operation)
            : Denied(normalized, operation, "feature_not_purchased");
    }

    private static string Normalize(string value)
    {
        return value.Trim().ToLowerInvariant();
    }

    private static FeatureAccessDecision Allowed(string featureCode, FeatureAccessOperation operation)
    {
        return new FeatureAccessDecision(true, null, featureCode, operation);
    }

    private static FeatureAccessDecision Denied(
        string featureCode,
        FeatureAccessOperation operation,
        string denialCode)
    {
        return new FeatureAccessDecision(false, denialCode, featureCode, operation);
    }
}

internal interface ITenantFeatureSnapshotProvider
{
    Task<TenantFeatureAccessSnapshot> GetAsync(
        Guid tenantId,
        FeatureAccessOperation operation,
        CancellationToken cancellationToken = default);
}

internal sealed class TenantFeatureSnapshotProvider(
    TikuDbContext dbContext,
    IMemoryCache memoryCache,
    IServiceProvider serviceProvider,
    ILogger<TenantFeatureSnapshotProvider> logger) : ITenantFeatureSnapshotProvider
{
    private static readonly TimeSpan MemoryDuration = TimeSpan.FromSeconds(30);

    private static readonly DistributedCacheEntryOptions DistributedOptions = new()
    {
        AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(60)
    };

    private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);

    private readonly
        Dictionary<(Guid TenantId, FeatureAccessOperation Operation), (long SaveVersion,
            Task<TenantFeatureAccessSnapshot> Snapshot)> requestCache = [];

    public Task<TenantFeatureAccessSnapshot> GetAsync(
        Guid tenantId,
        FeatureAccessOperation operation,
        CancellationToken cancellationToken = default)
    {
        var requestKey = (tenantId, operation);
        var saveVersion = dbContext.SaveVersion;
        if (!requestCache.TryGetValue(requestKey, out var cached) || cached.SaveVersion != saveVersion)
        {
            var snapshotTask = GetCoreAsync(
                tenantId,
                operation,
                cached.Snapshot is not null,
                cancellationToken);
            requestCache[requestKey] = (saveVersion, snapshotTask);
            return snapshotTask;
        }

        return cached.Snapshot;
    }

    private async Task<TenantFeatureAccessSnapshot> GetCoreAsync(
        Guid tenantId,
        FeatureAccessOperation operation,
        bool bypassSharedCache,
        CancellationToken cancellationToken)
    {
        var cacheKey = CacheKey(tenantId, operation);
        if (!bypassSharedCache &&
            memoryCache.TryGetValue<TenantFeatureAccessSnapshot>(cacheKey, out var memorySnapshot) &&
            memorySnapshot is not null)
            return memorySnapshot;

        var distributedCache = serviceProvider.GetService<IDistributedCache>();
        if (!bypassSharedCache && distributedCache is not null)
            try
            {
                var cached = await distributedCache.GetStringAsync(cacheKey, cancellationToken);
                if (!string.IsNullOrWhiteSpace(cached))
                {
                    var distributedSnapshot =
                        JsonSerializer.Deserialize<TenantFeatureAccessSnapshot>(cached, SerializerOptions);
                    if (distributedSnapshot is not null)
                    {
                        memoryCache.Set(cacheKey, distributedSnapshot, MemoryDuration);
                        return distributedSnapshot;
                    }
                }
            }
            catch (Exception exception) when (exception is not OperationCanceledException)
            {
                logger.LogWarning(exception, "Tenant feature snapshot cache read failed; falling back to PostgreSQL.");
            }

        var snapshot = await LoadAsync(tenantId, operation, cancellationToken);
        memoryCache.Set(cacheKey, snapshot, MemoryDuration);
        if (distributedCache is not null)
            try
            {
                await distributedCache.SetStringAsync(
                    cacheKey,
                    JsonSerializer.Serialize(snapshot, SerializerOptions),
                    DistributedOptions,
                    cancellationToken);
            }
            catch (Exception exception) when (exception is not OperationCanceledException)
            {
                logger.LogWarning(exception, "Tenant feature snapshot cache write failed; continuing without Redis.");
            }

        return snapshot;
    }

    private async Task<TenantFeatureAccessSnapshot> LoadAsync(
        Guid tenantId,
        FeatureAccessOperation operation,
        CancellationToken cancellationToken)
    {
        var tenant = await dbContext.Tenants.AsNoTracking()
            .Where(value => value.Id == tenantId)
            .Select(value => new
            {
                Status = (TenantStatus?)value.Status,
                Subscription = dbContext.TenantSaasSubscriptions.AsNoTracking()
                    .Where(subscription => subscription.TenantId == tenantId)
                    .OrderByDescending(subscription => subscription.UpdatedAt)
                    .Select(subscription => new TenantSubscriptionSnapshot(
                        subscription.Id,
                        subscription.BaseOfferingVersionId,
                        subscription.Status,
                        subscription.StartsAt,
                        subscription.CurrentPeriodEnd))
                    .FirstOrDefault()
            })
            .SingleOrDefaultAsync(cancellationToken);

        var features = await dbContext.SaasFeatures.AsNoTracking()
            .Where(value => value.Status == SaasFeatureStatus.Active)
            .Select(value => new TenantFeatureDefinition(value.Code, value.IsCore))
            .ToArrayAsync(cancellationToken);

        var now = DateTimeOffset.UtcNow;
        var overrides = await dbContext.TenantFeatureOverrides.AsNoTracking()
            .Where(value => value.TenantId == tenantId && (value.ExpiresAt == null || value.ExpiresAt > now))
            .ToDictionaryAsync(value => value.FeatureCode, value => value.Mode, StringComparer.Ordinal,
                cancellationToken);

        var purchased = new HashSet<string>(StringComparer.Ordinal);
        if (tenant?.Subscription is { } subscription)
        {
            var eligibleVersionIds = dbContext.TenantSaasSubscriptionItems.AsNoTracking()
                .Where(value => value.TenantId == tenantId && value.SubscriptionId == subscription.Id &&
                                (operation == FeatureAccessOperation.Read
                                    ? value.Status != TenantSaasSubscriptionItemStatus.Pending &&
                                      value.Status != TenantSaasSubscriptionItemStatus.Scheduled &&
                                      value.StartsAt <= now
                                    : value.Status == TenantSaasSubscriptionItemStatus.Active &&
                                      value.StartsAt <= now && value.EndsAt > now))
                .Select(value => value.OfferingVersionId)
                .Concat(dbContext.TenantSaasSubscriptions.AsNoTracking()
                    .Where(value => value.Id == subscription.Id)
                    .Select(value => value.BaseOfferingVersionId));
            purchased = (await dbContext.SaasOfferingVersionFeatures.AsNoTracking()
                    .Where(value => eligibleVersionIds.Contains(value.OfferingVersionId))
                    .Select(value => value.FeatureCode)
                    .Distinct()
                    .ToArrayAsync(cancellationToken))
                .ToHashSet(StringComparer.Ordinal);
        }

        return new TenantFeatureAccessSnapshot(
            tenant?.Status,
            tenant?.Subscription,
            features,
            overrides,
            purchased);
    }

    internal static string CacheKey(Guid tenantId, FeatureAccessOperation operation)
    {
        return $"tenant-feature-snapshot:v1:{tenantId:N}:{operation.ToString().ToLowerInvariant()}";
    }
}