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

namespace Tiku.Infrastructure.Security;

internal sealed class FeatureAccessService(TikuDbContext dbContext) : IFeatureAccessService
{
    public async Task<FeatureAccessDecision> EvaluateAsync(
        Guid tenantId,
        string featureCode,
        FeatureAccessOperation operation,
        CancellationToken cancellationToken = default)
    {
        var normalized = Normalize(featureCode);
        var tenantStatus = await dbContext.Tenants.AsNoTracking()
            .Where(value => value.Id == tenantId)
            .Select(value => (TenantStatus?)value.Status)
            .SingleOrDefaultAsync(cancellationToken);
        if (tenantStatus != TenantStatus.Active)
        {
            return Denied(normalized, operation, "tenant_inactive");
        }

        var feature = await dbContext.SaasFeatures.AsNoTracking()
            .Where(value => value.Code == normalized)
            .Select(value => new { value.Status, value.IsCore })
            .SingleOrDefaultAsync(cancellationToken);
        if (feature is null || feature.Status != SaasFeatureStatus.Active)
        {
            return Denied(normalized, operation, "feature_unavailable");
        }

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

        var now = DateTimeOffset.UtcNow;
        var overrideMode = await dbContext.TenantFeatureOverrides.AsNoTracking()
            .Where(value => value.TenantId == tenantId && value.FeatureCode == normalized &&
                            (value.ExpiresAt == null || value.ExpiresAt > now))
            .Select(value => (TenantFeatureOverrideMode?)value.Mode)
            .SingleOrDefaultAsync(cancellationToken);
        if (overrideMode == TenantFeatureOverrideMode.Disabled)
        {
            return Denied(normalized, operation, "feature_disabled");
        }

        var subscription = await dbContext.TenantSaasSubscriptions.AsNoTracking()
            .Where(value => value.TenantId == tenantId)
            .OrderByDescending(value => value.UpdatedAt)
            .Select(value => new
            {
                value.Id,
                value.BaseOfferingVersionId,
                value.Status,
                value.StartsAt,
                value.CurrentPeriodEnd
            })
            .FirstOrDefaultAsync(cancellationToken);
        if (subscription is null)
        {
            return overrideMode == TenantFeatureOverrideMode.Enabled
                ? Allowed(normalized, operation)
                : Denied(normalized, operation, "subscription_missing");
        }

        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 (overrideMode == TenantFeatureOverrideMode.Enabled)
        {
            return Allowed(normalized, operation);
        }

        var versionIds = await 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)
            .ToArrayAsync(cancellationToken);
        if (!versionIds.Contains(subscription.BaseOfferingVersionId))
        {
            versionIds = [.. versionIds, subscription.BaseOfferingVersionId];
        }

        var entitled = await dbContext.SaasOfferingVersionFeatures.AsNoTracking()
            .AnyAsync(value => versionIds.Contains(value.OfferingVersionId) && value.FeatureCode == normalized, cancellationToken);
        return entitled
            ? Allowed(normalized, operation)
            : Denied(normalized, operation, "feature_not_purchased");
    }

    public async Task<IReadOnlySet<string>> GetEnabledFeaturesAsync(
        Guid tenantId,
        FeatureAccessOperation operation = FeatureAccessOperation.Read,
        CancellationToken cancellationToken = default)
    {
        var codes = await dbContext.SaasFeatures.AsNoTracking()
            .Where(value => value.Status == SaasFeatureStatus.Active)
            .Select(value => value.Code)
            .ToArrayAsync(cancellationToken);
        var enabled = new HashSet<string>(StringComparer.Ordinal);
        foreach (var code in codes)
        {
            if ((await EvaluateAsync(tenantId, code, operation, cancellationToken)).Allowed)
            {
                enabled.Add(code);
            }
        }

        return enabled;
    }

    public async Task<IReadOnlySet<string>> FilterPermissionCodesAsync(
        Guid tenantId,
        IEnumerable<string> permissionCodes,
        FeatureAccessOperation operation = FeatureAccessOperation.Read,
        CancellationToken cancellationToken = default)
    {
        var requested = permissionCodes.Distinct(StringComparer.Ordinal).ToArray();
        var permissions = await (
            from permission in dbContext.BackendPermissions.AsNoTracking()
            join module in dbContext.PermissionModules.AsNoTracking()
                on permission.PermissionModuleCode equals module.Code
            where requested.Contains(permission.Code)
            select new { permission.Code, module.RequiredFeatureCode })
            .ToArrayAsync(cancellationToken);
        var allowed = new HashSet<string>(StringComparer.Ordinal);
        foreach (var permission in permissions)
        {
            if (permission.RequiredFeatureCode is null ||
                (await EvaluateAsync(tenantId, permission.RequiredFeatureCode, operation, cancellationToken)).Allowed)
            {
                allowed.Add(permission.Code);
            }
        }

        return allowed;
    }

    public async Task<IReadOnlyCollection<FeatureQuotaSnapshot>> GetQuotaSummaryAsync(
        Guid tenantId,
        CancellationToken cancellationToken = default)
    {
        var now = DateTimeOffset.UtcNow;
        var subscription = await CurrentWritableSubscriptionAsync(tenantId, now, cancellationToken);
        if (subscription is null)
        {
            return [];
        }

        var limits = await ResolveLimitsAsync(tenantId, subscription.Id, subscription.BaseOfferingVersionId, now, cancellationToken);
        var usages = await dbContext.TenantFeatureUsages.AsNoTracking()
            .Where(value => value.TenantId == tenantId && value.PeriodStart <= now && value.PeriodEnd > now)
            .ToDictionaryAsync(value => value.MetricCode, StringComparer.Ordinal, cancellationToken);
        var result = new List<FeatureQuotaSnapshot>();
        foreach (var limit in limits)
        {
            usages.TryGetValue(limit.Key, out var usage);
            var used = usage?.UsedValue ?? 0;
            var percent = limit.Value == 0 ? 100 : (int)Math.Min(100, used * 100 / limit.Value);
            result.Add(new FeatureQuotaSnapshot(
                limit.Key,
                used,
                limit.Value,
                percent,
                percent >= 80,
                used >= limit.Value,
                usage?.PeriodStart ?? subscription.CurrentPeriodStart,
                usage?.PeriodEnd ?? subscription.CurrentPeriodEnd));
        }

        return result.OrderBy(value => value.MetricCode, StringComparer.Ordinal).ToArray();
    }

    public async Task<bool> TryConsumeQuotaAsync(
        Guid tenantId,
        string metricCode,
        long amount,
        CancellationToken cancellationToken = default)
    {
        if (amount <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(amount));
        }

        var normalized = Normalize(metricCode);
        var now = DateTimeOffset.UtcNow;
        var subscription = await CurrentWritableSubscriptionAsync(tenantId, now, cancellationToken);
        if (subscription is null)
        {
            return false;
        }

        var limits = await ResolveLimitsAsync(tenantId, subscription.Id, subscription.BaseOfferingVersionId, now, cancellationToken);
        if (!limits.TryGetValue(normalized, out var limit))
        {
            // Quotas are opt-in per offering version. A missing metric means that the
            // subscription does not cap this operation, rather than a zero allowance.
            return true;
        }

        var updated = await dbContext.TenantFeatureUsages
            .Where(value => value.TenantId == tenantId && value.MetricCode == normalized &&
                            value.PeriodStart == subscription.CurrentPeriodStart &&
                            value.PeriodEnd == subscription.CurrentPeriodEnd &&
                            value.UsedValue + amount <= limit)
            .ExecuteUpdateAsync(setters => setters
                .SetProperty(value => value.UsedValue, value => value.UsedValue + amount)
                .SetProperty(value => value.LimitValueSnapshot, limit)
                .SetProperty(value => value.WarningIssued, value => value.WarningIssued || (value.UsedValue + amount) * 100 >= limit * 80)
                .SetProperty(value => value.Version, value => value.Version + 1)
                .SetProperty(value => value.UpdatedAt, now), cancellationToken);
        if (updated == 1)
        {
            return true;
        }

        var exists = await dbContext.TenantFeatureUsages.AnyAsync(value =>
            value.TenantId == tenantId && value.MetricCode == normalized &&
            value.PeriodStart == subscription.CurrentPeriodStart && value.PeriodEnd == subscription.CurrentPeriodEnd,
            cancellationToken);
        if (exists || amount > limit)
        {
            return false;
        }

        dbContext.TenantFeatureUsages.Add(new TenantFeatureUsage
        {
            TenantId = tenantId,
            MetricCode = normalized,
            PeriodStart = subscription.CurrentPeriodStart,
            PeriodEnd = subscription.CurrentPeriodEnd,
            UsedValue = amount,
            LimitValueSnapshot = limit,
            WarningIssued = amount * 100 >= limit * 80,
            Version = 1
        });
        try
        {
            await dbContext.SaveChangesAsync(cancellationToken);
            return true;
        }
        catch (DbUpdateException exception)
        {
            foreach (var entry in exception.Entries)
            {
                entry.State = EntityState.Detached;
            }
            return await dbContext.TenantFeatureUsages
                .Where(value => value.TenantId == tenantId && value.MetricCode == normalized &&
                                value.PeriodStart == subscription.CurrentPeriodStart &&
                                value.PeriodEnd == subscription.CurrentPeriodEnd &&
                                value.UsedValue + amount <= limit)
                .ExecuteUpdateAsync(setters => setters
                    .SetProperty(value => value.UsedValue, value => value.UsedValue + amount)
                    .SetProperty(value => value.Version, value => value.Version + 1)
                    .SetProperty(value => value.UpdatedAt, now), cancellationToken) == 1;
        }
    }

    public async Task ReleaseQuotaAsync(
        Guid tenantId,
        string metricCode,
        long amount,
        CancellationToken cancellationToken = default)
    {
        if (amount <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(amount));
        }

        var normalized = Normalize(metricCode);
        var now = DateTimeOffset.UtcNow;
        var subscription = await CurrentWritableSubscriptionAsync(tenantId, now, cancellationToken);
        if (subscription is null)
        {
            return;
        }
        await dbContext.TenantFeatureUsages
            .Where(value => value.TenantId == tenantId && value.MetricCode == normalized &&
                            value.PeriodStart == subscription.CurrentPeriodStart &&
                            value.PeriodEnd == subscription.CurrentPeriodEnd)
            .ExecuteUpdateAsync(setters => setters
                .SetProperty(value => value.UsedValue, value => value.UsedValue > amount ? value.UsedValue - amount : 0)
                .SetProperty(value => value.Version, value => value.Version + 1)
                .SetProperty(value => value.UpdatedAt, now), cancellationToken);
    }

    private async Task<SubscriptionProjection?> CurrentWritableSubscriptionAsync(
        Guid tenantId,
        DateTimeOffset now,
        CancellationToken cancellationToken)
    {
        return await dbContext.TenantSaasSubscriptions.AsNoTracking()
            .Where(value => value.TenantId == tenantId &&
                            (value.Status == TenantSaasSubscriptionStatus.Trial ||
                             value.Status == TenantSaasSubscriptionStatus.Active))
            .Where(value => value.StartsAt <= now && value.CurrentPeriodEnd > now)
            .OrderByDescending(value => value.UpdatedAt)
            .Select(value => new SubscriptionProjection(
                value.Id,
                value.BaseOfferingVersionId,
                value.CurrentPeriodStart,
                value.CurrentPeriodEnd))
            .FirstOrDefaultAsync(cancellationToken);
    }

    private async Task<Dictionary<string, long>> ResolveLimitsAsync(
        Guid tenantId,
        Guid subscriptionId,
        Guid baseVersionId,
        DateTimeOffset now,
        CancellationToken cancellationToken)
    {
        var versionIds = await dbContext.TenantSaasSubscriptionItems.AsNoTracking()
            .Where(value => value.TenantId == tenantId && value.SubscriptionId == subscriptionId &&
                            value.Status == TenantSaasSubscriptionItemStatus.Active &&
                            value.StartsAt <= now && value.EndsAt > now)
            .Select(value => value.OfferingVersionId)
            .ToArrayAsync(cancellationToken);
        if (!versionIds.Contains(baseVersionId))
        {
            versionIds = [.. versionIds, baseVersionId];
        }

        return await dbContext.SaasOfferingVersionLimits.AsNoTracking()
            .Where(value => versionIds.Contains(value.OfferingVersionId))
            .GroupBy(value => value.MetricCode)
            .Select(group => new { MetricCode = group.Key, Limit = group.Sum(value => value.LimitValue) })
            .ToDictionaryAsync(value => value.MetricCode, value => value.Limit, StringComparer.Ordinal, cancellationToken);
    }

    private static FeatureAccessDecision Allowed(string featureCode, FeatureAccessOperation operation) =>
        new(true, null, featureCode, operation);

    private static FeatureAccessDecision Denied(string featureCode, FeatureAccessOperation operation, string code) =>
        new(false, code, featureCode, operation);

    private static string Normalize(string value) => value.Trim().ToLowerInvariant();

    private sealed record SubscriptionProjection(
        Guid Id,
        Guid BaseOfferingVersionId,
        DateTimeOffset CurrentPeriodStart,
        DateTimeOffset CurrentPeriodEnd);
}
