using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Tiku.Application.Security;
using Tiku.Domain.Content;
using Tiku.Domain.Platform;
using Tiku.Domain.QuestionBanks;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;

namespace Tiku.Infrastructure.Security;

internal sealed class FeatureUsageReconciliationService(
    TikuDbContext dbContext,
    ITenantExecutionScope tenantExecutionScope,
    IOptions<FeatureUsageReconciliationOptions> options) : IFeatureUsageReconciliationService
{
    private static readonly string[] CurrentMetrics =
    [
        SaasQuotaMetricCatalog.StaffCount,
        SaasQuotaMetricCatalog.StudentCount,
        SaasQuotaMetricCatalog.PrivateQuestionCount,
        SaasQuotaMetricCatalog.StorageBytes
    ];

    public Task<IReadOnlyCollection<ReconciledFeatureUsage>> ReconcileTenantAsync(
        ReconcileFeatureUsageRequest request,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(request);
        return tenantExecutionScope.ExecuteAsync<IReadOnlyCollection<ReconciledFeatureUsage>>(
            new SystemScopeRequest(
                request.TenantId,
                request.CallerType,
                request.Caller,
                request.Reason,
                request.CorrelationId),
            async (provider, token) =>
            {
                var dbContext = provider.GetRequiredService<TikuDbContext>();
                return await ReconcileCoreAsync(dbContext, request.TenantId, token);
            },
            cancellationToken);
    }

    public async Task<int> ProcessDueAsync(CancellationToken cancellationToken = default)
    {
        if (!options.Value.Enabled) return 0;

        var now = DateTimeOffset.UtcNow;
        var cutoff = now.AddMinutes(-Math.Clamp(options.Value.IntervalMinutes, 1, 24 * 60));
        var candidates = await dbContext.TenantSaasSubscriptions.AsNoTracking()
            .Where(subscription =>
                (subscription.Status == TenantSaasSubscriptionStatus.Trial ||
                 subscription.Status == TenantSaasSubscriptionStatus.Active) &&
                subscription.StartsAt <= now &&
                subscription.CurrentPeriodEnd > now &&
                dbContext.TenantSaasSubscriptionItems.Any(item =>
                    item.TenantId == subscription.TenantId &&
                    item.SubscriptionId == subscription.Id &&
                    item.Status == TenantSaasSubscriptionItemStatus.Active &&
                    item.StartsAt <= now &&
                    item.EndsAt > now &&
                    dbContext.SaasOfferingVersionLimits.Any(limit =>
                        limit.OfferingVersionId == item.OfferingVersionId &&
                        CurrentMetrics.Contains(limit.MetricCode))) &&
                !dbContext.AuditLogs.Any(audit =>
                    audit.TenantId == subscription.TenantId &&
                    audit.Action == "system_scope.completed" &&
                    audit.TargetId != null &&
                    audit.TargetId.StartsWith("feature-usage-periodic-") &&
                    audit.CreatedAt >= cutoff))
            .OrderBy(subscription => subscription.UpdatedAt)
            .Select(subscription => subscription.TenantId)
            .Distinct()
            .Take(Math.Clamp(options.Value.BatchSize, 1, 1000))
            .ToArrayAsync(cancellationToken);

        foreach (var tenantId in candidates)
            await ReconcileTenantAsync(
                new ReconcileFeatureUsageRequest(
                    tenantId,
                    SystemScopeCallerType.Worker,
                    nameof(FeatureUsageReconciliationService),
                    "Periodic tenant feature usage reconciliation",
                    $"feature-usage-periodic-{tenantId:N}-{now:yyyyMMddHHmmss}"),
                cancellationToken);

        return candidates.Length;
    }

    private static async Task<IReadOnlyCollection<ReconciledFeatureUsage>> ReconcileCoreAsync(
        TikuDbContext dbContext,
        Guid tenantId,
        CancellationToken cancellationToken)
    {
        var now = DateTimeOffset.UtcNow;
        var subscription = await dbContext.TenantSaasSubscriptions.AsNoTracking()
            .Where(item => item.TenantId == tenantId &&
                           (item.Status == TenantSaasSubscriptionStatus.Trial ||
                            item.Status == TenantSaasSubscriptionStatus.Active) &&
                           item.StartsAt <= now &&
                           item.CurrentPeriodEnd > now)
            .OrderByDescending(item => item.UpdatedAt)
            .Select(item => new
            {
                item.Id,
                item.BaseOfferingVersionId,
                item.CurrentPeriodStart,
                item.CurrentPeriodEnd
            })
            .FirstOrDefaultAsync(cancellationToken);
        if (subscription is null) return [];

        var versionIds = await dbContext.TenantSaasSubscriptionItems.AsNoTracking()
            .Where(item => item.TenantId == tenantId &&
                           item.SubscriptionId == subscription.Id &&
                           item.Status == TenantSaasSubscriptionItemStatus.Active &&
                           item.StartsAt <= now &&
                           item.EndsAt > now)
            .Select(item => item.OfferingVersionId)
            .ToArrayAsync(cancellationToken);
        if (!versionIds.Contains(subscription.BaseOfferingVersionId))
            versionIds = [.. versionIds, subscription.BaseOfferingVersionId];

        var limits = await dbContext.SaasOfferingVersionLimits.AsNoTracking()
            .Where(item => versionIds.Contains(item.OfferingVersionId) && CurrentMetrics.Contains(item.MetricCode))
            .GroupBy(item => item.MetricCode)
            .Select(group => new { MetricCode = group.Key, LimitValue = group.Sum(item => item.LimitValue) })
            .ToDictionaryAsync(item => item.MetricCode, item => item.LimitValue, StringComparer.Ordinal,
                cancellationToken);
        if (limits.Count == 0) return [];

        var actual = new Dictionary<string, long>(StringComparer.Ordinal)
        {
            [SaasQuotaMetricCatalog.StaffCount] = await dbContext.TenantMemberships.AsNoTracking()
                .Where(item => item.TenantId == tenantId &&
                               item.Status == MembershipStatus.Active &&
                               item.Role != TenantRole.Student)
                .Select(item => item.UserId)
                .Distinct()
                .LongCountAsync(cancellationToken),
            [SaasQuotaMetricCatalog.StudentCount] = await dbContext.TenantMemberships.AsNoTracking()
                .Where(item => item.TenantId == tenantId &&
                               item.Status == MembershipStatus.Active &&
                               item.Role == TenantRole.Student)
                .Select(item => item.UserId)
                .Distinct()
                .LongCountAsync(cancellationToken),
            [SaasQuotaMetricCatalog.PrivateQuestionCount] = await dbContext.Questions.AsNoTracking()
                .LongCountAsync(item => item.TenantId == tenantId && item.Status != QuestionStatus.Archived,
                    cancellationToken),
            [SaasQuotaMetricCatalog.StorageBytes] = await dbContext.ContentAssets.AsNoTracking()
                .Where(item => item.TenantId == tenantId &&
                               item.Status == ContentStatus.Active &&
                               item.VerifiedSizeBytes > 0)
                .SumAsync(item => item.VerifiedSizeBytes ?? 0, cancellationToken)
        };

        var existing = await dbContext.TenantFeatureUsages
            .Where(item => item.TenantId == tenantId &&
                           item.PeriodStart == subscription.CurrentPeriodStart &&
                           item.PeriodEnd == subscription.CurrentPeriodEnd &&
                           CurrentMetrics.Contains(item.MetricCode))
            .ToDictionaryAsync(item => item.MetricCode, StringComparer.Ordinal, cancellationToken);
        var result = new List<ReconciledFeatureUsage>();
        foreach (var limit in limits.OrderBy(item => item.Key, StringComparer.Ordinal))
        {
            var actualValue = actual[limit.Key];
            var warning = limit.Value == 0 || actualValue * 100 >= limit.Value * 80;
            var exceeded = actualValue >= limit.Value;
            var isNew = !existing.TryGetValue(limit.Key, out var usage);
            if (isNew)
            {
                usage = new TenantFeatureUsage
                {
                    TenantId = tenantId,
                    MetricCode = limit.Key,
                    PeriodStart = subscription.CurrentPeriodStart,
                    PeriodEnd = subscription.CurrentPeriodEnd,
                    Version = 1
                };
                dbContext.TenantFeatureUsages.Add(usage);
            }

            var trackedUsage = usage!;
            if (trackedUsage.UsedValue != actualValue ||
                trackedUsage.LimitValueSnapshot != limit.Value ||
                trackedUsage.WarningIssued != warning)
            {
                trackedUsage.UsedValue = actualValue;
                trackedUsage.LimitValueSnapshot = limit.Value;
                trackedUsage.WarningIssued = warning;
                trackedUsage.Version = isNew ? 1 : trackedUsage.Version + 1;
            }

            result.Add(new ReconciledFeatureUsage(limit.Key, actualValue, limit.Value, warning, exceeded));
        }

        await dbContext.SaveChangesAsync(cancellationToken);
        return result;
    }
}