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

namespace Tiku.Infrastructure.Security;

internal sealed class FeatureAccessService(
    TikuDbContext dbContext,
    ITenantFeatureSnapshotProvider snapshotProvider) : IFeatureAccessService
{
    public async Task<FeatureAccessDecision> EvaluateAsync(
        Guid tenantId,
        string featureCode,
        FeatureAccessOperation operation,
        CancellationToken cancellationToken = default)
    {
        return (await snapshotProvider.GetAsync(tenantId, operation, cancellationToken))
            .Evaluate(featureCode, operation);
    }

    public async Task<IReadOnlySet<string>> GetEnabledFeaturesAsync(
        Guid tenantId,
        FeatureAccessOperation operation = FeatureAccessOperation.Read,
        CancellationToken cancellationToken = default)
    {
        var snapshot = await snapshotProvider.GetAsync(tenantId, operation, cancellationToken);
        return snapshot.Features
            .Where(feature => snapshot.Evaluate(feature.Code, operation).Allowed)
            .Select(feature => feature.Code)
            .ToHashSet(StringComparer.Ordinal);
    }

    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 snapshot = await snapshotProvider.GetAsync(tenantId, operation, cancellationToken);
        var allowed = new HashSet<string>(StringComparer.Ordinal);
        foreach (var permission in permissions)
            if (permission.RequiredFeatureCode is null ||
                snapshot.Evaluate(permission.RequiredFeatureCode, operation).Allowed)
                allowed.Add(permission.Code);

        return allowed;
    }

    public async Task<IReadOnlyCollection<FeatureQuotaSnapshot>> GetQuotaSummaryAsync(
        Guid tenantId,
        CancellationToken cancellationToken = default)
    {
        var now = DateTimeOffset.UtcNow;
        var rows = await dbContext.Database.SqlQuery<QuotaSummaryProjection>($"""
                                                                              WITH current_subscription AS (
                                                                                  SELECT subscription.id,
                                                                                         subscription.base_offering_version_id,
                                                                                         subscription.current_period_start,
                                                                                         subscription.current_period_end
                                                                                  FROM tenant_saas_subscriptions AS subscription
                                                                                  WHERE subscription.tenant_id = {tenantId}
                                                                                    AND subscription.status IN ('trial', 'active')
                                                                                    AND subscription.starts_at <= {now}
                                                                                    AND subscription.current_period_end > {now}
                                                                                  ORDER BY subscription.updated_at DESC
                                                                                  LIMIT 1
                                                                              ),
                                                                              version_ids AS (
                                                                                  SELECT subscription.base_offering_version_id AS offering_version_id
                                                                                  FROM current_subscription AS subscription
                                                                                  UNION
                                                                                  SELECT item.offering_version_id
                                                                                  FROM tenant_saas_subscription_items AS item
                                                                                  INNER JOIN current_subscription AS subscription
                                                                                      ON subscription.id = item.subscription_id
                                                                                  WHERE item.tenant_id = {tenantId}
                                                                                    AND item.status = 'active'
                                                                                    AND item.starts_at <= {now}
                                                                                    AND item.ends_at > {now}
                                                                              ),
                                                                              quota_limits AS (
                                                                                  SELECT definition.metric_code,
                                                                                         sum(definition.limit_value)::bigint AS limit_value
                                                                                  FROM saas_offering_version_limits AS definition
                                                                                  WHERE definition.offering_version_id IN (
                                                                                      SELECT version.offering_version_id FROM version_ids AS version)
                                                                                  GROUP BY definition.metric_code
                                                                              )
                                                                              SELECT limits.metric_code AS "MetricCode",
                                                                                     COALESCE(usage.used_value, 0)::bigint AS "Used",
                                                                                     limits.limit_value AS "Limit",
                                                                                     COALESCE(usage.period_start, subscription.current_period_start) AS "PeriodStart",
                                                                                     COALESCE(usage.period_end, subscription.current_period_end) AS "PeriodEnd"
                                                                              FROM quota_limits AS limits
                                                                              CROSS JOIN current_subscription AS subscription
                                                                              LEFT JOIN LATERAL (
                                                                                  SELECT current_usage.used_value,
                                                                                         current_usage.period_start,
                                                                                         current_usage.period_end
                                                                                  FROM tenant_feature_usage AS current_usage
                                                                                  WHERE current_usage.tenant_id = {tenantId}
                                                                                    AND current_usage.metric_code = limits.metric_code
                                                                                    AND current_usage.period_start <= {now}
                                                                                    AND current_usage.period_end > {now}
                                                                                  ORDER BY current_usage.period_start DESC
                                                                                  LIMIT 1
                                                                              ) AS usage ON TRUE
                                                                              ORDER BY limits.metric_code
                                                                              """)
            .ToArrayAsync(cancellationToken);
        var result = new List<FeatureQuotaSnapshot>(rows.Length);
        foreach (var row in rows)
        {
            var percent = row.Limit == 0 ? 100 : (int)Math.Min(100, row.Used * 100 / row.Limit);
            result.Add(new FeatureQuotaSnapshot(
                row.MetricCode,
                row.Used,
                row.Limit,
                percent,
                percent >= 80,
                row.Used >= row.Limit,
                row.PeriodStart,
                row.PeriodEnd));
        }

        return result;
    }

    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)
    {
        return new FeatureAccessDecision(true, null, featureCode, operation);
    }

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

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

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

    private sealed record QuotaSummaryProjection(
        string MetricCode,
        long Used,
        long Limit,
        DateTimeOffset PeriodStart,
        DateTimeOffset PeriodEnd);
}