using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Tiku.Application.Commerce;
using Tiku.Application.PlatformBilling;
using Tiku.Application.Security;
using Tiku.Domain.Common;
using Tiku.Domain.Operations;
using Tiku.Domain.Platform;
using Tiku.Infrastructure.Persistence;

namespace Tiku.Infrastructure.PlatformBilling;

internal sealed class TenantBillingService(
    TikuDbContext dbContext,
    IPlatformBillingPaymentGateway paymentGateway,
    IPlatformBillingSettlementService settlementService,
    IFeatureAccessService featureAccessService,
    IConfiguration configuration) : ITenantBillingService
{
    private static readonly TimeSpan QuoteLifetime = TimeSpan.FromMinutes(30);
    private static readonly TimeSpan OrderLifetime = TimeSpan.FromHours(2);

    public async Task<TenantBillingCatalog> GetCatalogAsync(
        TenantBillingActor actor,
        CancellationToken cancellationToken = default)
    {
        var now = DateTimeOffset.UtcNow;
        var features = await dbContext.SaasFeatures.AsNoTracking()
            .Where(value => value.Status == SaasFeatureStatus.Active && !value.IsCore)
            .OrderBy(value => value.SortOrder).ThenBy(value => value.Code)
            .ToArrayAsync(cancellationToken);
        var versions = await LoadPublishedVersionsAsync(now, cancellationToken);
        return new TenantBillingCatalog(
            features,
            versions.Where(value => value.OfferingType == SaasOfferingType.BasePlan).ToArray(),
            versions.Where(value => value.OfferingType == SaasOfferingType.AddOn).ToArray());
    }

    public async Task<PlatformBillingQuoteView> CreateQuoteAsync(
        TenantBillingActor actor,
        CreatePlatformBillingQuoteCommand command,
        CancellationToken cancellationToken = default)
    {
        var idempotencyKey = Required(command.IdempotencyKey, "idempotencyKey");
        var existingQuote = await dbContext.PlatformBillingQuotes.AsNoTracking()
            .SingleOrDefaultAsync(value => value.TenantId == actor.TenantId && value.IdempotencyKey == idempotencyKey,
                cancellationToken);
        if (existingQuote is not null) return await LoadQuoteAsync(actor.TenantId, existingQuote.Id, cancellationToken);

        var requestedIds = new[] { command.BaseOfferingVersionId }
            .Concat(command.AddOnOfferingVersionIds)
            .Distinct()
            .ToArray();
        var now = DateTimeOffset.UtcNow;
        var versions = await (
                from version in dbContext.SaasOfferingVersions.AsNoTracking()
                join offering in dbContext.SaasOfferings.AsNoTracking() on version.OfferingId equals offering.Id
                where requestedIds.Contains(version.Id) &&
                      version.Status == SaasOfferingVersionStatus.Published &&
                      offering.Status == SaasOfferingStatus.Active &&
                      (version.EffectiveAt == null || version.EffectiveAt <= now)
                select new { Version = version, Offering = offering })
            .ToArrayAsync(cancellationToken);
        if (versions.Length != requestedIds.Length ||
            versions.SingleOrDefault(value => value.Version.Id == command.BaseOfferingVersionId)?.Offering.Type !=
            SaasOfferingType.BasePlan ||
            versions.Any(value =>
                value.Version.Id != command.BaseOfferingVersionId && value.Offering.Type != SaasOfferingType.AddOn))
            throw Error("One or more offering versions are unavailable.", "saas_offering_version_unavailable");
        if (versions.Select(value => value.Version.Currency).Distinct(StringComparer.Ordinal).Count() != 1)
            throw Error("All quote items must use the same currency.", "platform_billing_currency_mismatch");

        var featureRows = await dbContext.SaasOfferingVersionFeatures.AsNoTracking()
            .Where(value => requestedIds.Contains(value.OfferingVersionId))
            .ToArrayAsync(cancellationToken);
        var limitRows = await dbContext.SaasOfferingVersionLimits.AsNoTracking()
            .Where(value => requestedIds.Contains(value.OfferingVersionId))
            .ToArrayAsync(cancellationToken);
        var featureCodes = featureRows.Select(value => value.FeatureCode).Distinct(StringComparer.Ordinal)
            .Order(StringComparer.Ordinal).ToArray();
        var limits = limitRows.GroupBy(value => value.MetricCode, StringComparer.Ordinal)
            .ToDictionary(group => group.Key, group => group.Sum(value => value.LimitValue), StringComparer.Ordinal);
        var originalAmount = versions.Sum(value => value.Version.OriginalAmountCents);
        var totalAmount = versions.Sum(value => value.Version.AmountCents);
        var quote = new PlatformBillingQuote
        {
            TenantId = actor.TenantId,
            QuoteNo = Number("SQ"),
            IdempotencyKey = idempotencyKey,
            Purpose = command.Purpose,
            OriginalAmountCents = originalAmount,
            DiscountAmountCents = originalAmount - totalAmount,
            TotalAmountCents = totalAmount,
            Currency = versions[0].Version.Currency,
            ExpiresAt = now.Add(QuoteLifetime),
            FeatureSnapshot = JsonSerializer.SerializeToElement(featureCodes),
            LimitSnapshot = JsonSerializer.SerializeToElement(limits)
        };
        dbContext.PlatformBillingQuotes.Add(quote);
        dbContext.PlatformBillingQuoteItems.AddRange(versions.Select(value => new PlatformBillingQuoteItem
        {
            TenantId = actor.TenantId,
            QuoteId = quote.Id,
            OfferingVersionId = value.Version.Id,
            ItemType = value.Offering.Type == SaasOfferingType.BasePlan
                ? PlatformBillingItemType.BasePlan
                : PlatformBillingItemType.AddOn,
            Quantity = 1,
            UnitAmountCents = value.Version.AmountCents,
            AmountCents = value.Version.AmountCents,
            Snapshot = JsonSerializer.SerializeToElement(new
            {
                offeringId = value.Offering.Id,
                offeringCode = value.Offering.Code,
                offeringName = value.Offering.Name,
                offeringType = value.Offering.Type,
                version = value.Version.Version,
                value.Version.BillingCycle,
                featureCodes = featureRows.Where(feature => feature.OfferingVersionId == value.Version.Id)
                    .Select(feature => feature.FeatureCode).Order(StringComparer.Ordinal),
                limits = limitRows.Where(limit => limit.OfferingVersionId == value.Version.Id)
                    .ToDictionary(limit => limit.MetricCode, limit => limit.LimitValue)
            })
        }));
        await dbContext.SaveChangesAsync(cancellationToken);
        return await LoadQuoteAsync(actor.TenantId, quote.Id, cancellationToken);
    }

    public async Task<PlatformBillingOrderView> CreateOrderAsync(
        TenantBillingActor actor,
        CreatePlatformBillingOrderCommand command,
        CancellationToken cancellationToken = default)
    {
        var key = Required(command.IdempotencyKey, "idempotencyKey");
        var existing = await dbContext.PlatformBillingOrders.AsNoTracking()
            .SingleOrDefaultAsync(value => value.TenantId == actor.TenantId && value.IdempotencyKey == key,
                cancellationToken);
        if (existing is not null) return await LoadOrderAsync(actor.TenantId, existing.OrderNo, cancellationToken);

        var now = DateTimeOffset.UtcNow;
        var quote = await dbContext.PlatformBillingQuotes.SingleOrDefaultAsync(value =>
                        value.TenantId == actor.TenantId && value.Id == command.QuoteId, cancellationToken)
                    ?? throw Error("Quote was not found.", "platform_billing_quote_not_found");
        if (quote.Status != PlatformBillingQuoteStatus.Active || quote.ExpiresAt <= now)
        {
            quote.Status = quote.ExpiresAt <= now ? PlatformBillingQuoteStatus.Expired : quote.Status;
            await dbContext.SaveChangesAsync(cancellationToken);
            throw Error("Quote is no longer active.", "platform_billing_quote_expired");
        }

        var quoteItems = await dbContext.PlatformBillingQuoteItems.AsNoTracking()
            .Where(value => value.TenantId == actor.TenantId && value.QuoteId == quote.Id)
            .ToArrayAsync(cancellationToken);
        var order = new PlatformBillingOrder
        {
            TenantId = actor.TenantId,
            QuoteId = quote.Id,
            OrderNo = Number("SO"),
            IdempotencyKey = key,
            Purpose = quote.Purpose,
            OriginalAmountCents = quote.OriginalAmountCents,
            DiscountAmountCents = quote.DiscountAmountCents,
            TotalAmountCents = quote.TotalAmountCents,
            Currency = quote.Currency,
            ExpiresAt = now.Add(OrderLifetime),
            Snapshot = JsonSerializer.SerializeToElement(new
            {
                quote.QuoteNo,
                quote.FeatureSnapshot,
                quote.LimitSnapshot
            })
        };
        dbContext.PlatformBillingOrders.Add(order);
        dbContext.PlatformBillingOrderItems.AddRange(quoteItems.Select(value => new PlatformBillingOrderItem
        {
            TenantId = actor.TenantId,
            OrderId = order.Id,
            OfferingVersionId = value.OfferingVersionId,
            ItemType = value.ItemType,
            Quantity = value.Quantity,
            UnitAmountCents = value.UnitAmountCents,
            AmountCents = value.AmountCents,
            Snapshot = value.Snapshot
        }));
        quote.Status = PlatformBillingQuoteStatus.Converted;
        await dbContext.SaveChangesAsync(cancellationToken);
        return await LoadOrderAsync(actor.TenantId, order.OrderNo, cancellationToken);
    }

    public async Task<PlatformBillingPaymentView> CreatePaymentAsync(
        TenantBillingActor actor,
        CreatePlatformBillingPaymentCommand command,
        CancellationToken cancellationToken = default)
    {
        var key = Required(command.IdempotencyKey, "idempotencyKey");
        var existing = await dbContext.PlatformBillingPayments.AsNoTracking()
            .SingleOrDefaultAsync(value => value.TenantId == actor.TenantId && value.IdempotencyKey == key,
                cancellationToken);
        if (existing is not null) return ToPaymentView(existing);

        var order = await dbContext.PlatformBillingOrders.SingleOrDefaultAsync(value =>
                        value.TenantId == actor.TenantId && value.OrderNo == command.OrderNo, cancellationToken)
                    ?? throw Error("Order was not found.", "platform_billing_order_not_found");
        if (order.Status != PlatformBillingOrderStatus.PendingPayment || order.ExpiresAt <= DateTimeOffset.UtcNow)
        {
            if (order.ExpiresAt <= DateTimeOffset.UtcNow && order.Status == PlatformBillingOrderStatus.PendingPayment)
            {
                order.Status = PlatformBillingOrderStatus.Expired;
                await dbContext.SaveChangesAsync(cancellationToken);
            }

            throw Error("Order does not allow a new payment.", "platform_billing_order_status_invalid");
        }

        var provider = NormalizeProvider(command.Provider);
        var payment = new PlatformBillingPayment
        {
            TenantId = actor.TenantId,
            OrderId = order.Id,
            PaymentNo = Number("SP"),
            IdempotencyKey = key,
            Provider = provider,
            Method = Required(command.Method, "method"),
            AmountCents = order.TotalAmountCents
        };
        dbContext.PlatformBillingPayments.Add(payment);
        await dbContext.SaveChangesAsync(cancellationToken);

        if (order.TotalAmountCents == 0)
        {
            await settlementService.MarkPaidAsync(
                payment.Id,
                $"zero-{payment.Id:N}",
                "zero_amount_settled",
                null,
                DateTimeOffset.UtcNow,
                JsonDefaults.Object(),
                actor.UserId,
                cancellationToken);
            return ToPaymentView(payment);
        }

        try
        {
            var result = await paymentGateway.CreatePaymentAsync(
                provider,
                new CreatePaymentProviderRequest(
                    actor.TenantId,
                    order.OrderNo,
                    $"SaaS subscription {order.OrderNo}",
                    order.TotalAmountCents,
                    payment.Method,
                    command.OpenId,
                    command.ReturnUrl,
                    command.QuitUrl,
                    provider == PaymentProviders.Manual ? string.Empty : BuildNotifyUrl(provider),
                    JsonSerializer.SerializeToElement(new { payment.PaymentNo })),
                cancellationToken);
            payment.ProviderTradeNo = result.ProviderTradeNo;
            payment.ClientPayload = result.ClientPayload;
            await dbContext.SaveChangesAsync(cancellationToken);
            return ToPaymentView(payment);
        }
        catch
        {
            payment.Status = PlatformBillingPaymentStatus.Failed;
            await dbContext.SaveChangesAsync(CancellationToken.None);
            throw;
        }
    }

    public async Task<IReadOnlyCollection<PlatformBillingOrderView>> GetOrdersAsync(
        TenantBillingActor actor,
        int limit,
        CancellationToken cancellationToken = default)
    {
        var orderNos = await dbContext.PlatformBillingOrders.AsNoTracking()
            .Where(value => value.TenantId == actor.TenantId)
            .OrderByDescending(value => value.CreatedAt)
            .Take(Math.Clamp(limit, 1, 200))
            .Select(value => value.OrderNo)
            .ToArrayAsync(cancellationToken);
        var result = new List<PlatformBillingOrderView>();
        foreach (var orderNo in orderNos) result.Add(await LoadOrderAsync(actor.TenantId, orderNo, cancellationToken));
        return result;
    }

    public Task<PlatformBillingOrderView> GetOrderAsync(TenantBillingActor actor, string orderNo,
        CancellationToken cancellationToken = default)
    {
        return LoadOrderAsync(actor.TenantId, orderNo, cancellationToken);
    }

    public async Task<TenantSubscriptionView?> GetSubscriptionAsync(
        TenantBillingActor actor,
        CancellationToken cancellationToken = default)
    {
        var subscription = await dbContext.TenantSaasSubscriptions.AsNoTracking()
            .Where(value => value.TenantId == actor.TenantId)
            .OrderByDescending(value => value.UpdatedAt)
            .FirstOrDefaultAsync(cancellationToken);
        return subscription is null ? null : await ToSubscriptionViewAsync(subscription, cancellationToken);
    }

    public async Task<PlatformBillingOrderView> ChangeSubscriptionAsync(
        TenantBillingActor actor,
        ChangeTenantSubscriptionCommand command,
        string idempotencyKey,
        CancellationToken cancellationToken = default)
    {
        var current = await dbContext.TenantSaasSubscriptions.AsNoTracking()
            .Where(value => value.TenantId == actor.TenantId)
            .OrderByDescending(value => value.UpdatedAt)
            .FirstOrDefaultAsync(cancellationToken);
        var currentAmount = current is null
            ? 0
            : await dbContext.SaasOfferingVersions.AsNoTracking()
                .Where(value => value.Id == current.BaseOfferingVersionId).Select(value => value.AmountCents)
                .SingleAsync(cancellationToken);
        var requestedAmount = await dbContext.SaasOfferingVersions.AsNoTracking()
                                  .Where(value => value.Id == command.BaseOfferingVersionId)
                                  .Select(value => (int?)value.AmountCents).SingleOrDefaultAsync(cancellationToken)
                              ?? throw Error("Offering version was not found.", "saas_offering_version_not_found");
        var purpose = current is null
            ? PlatformBillingOrderPurpose.NewSubscription
            : requestedAmount >= currentAmount
                ? PlatformBillingOrderPurpose.Upgrade
                : PlatformBillingOrderPurpose.Downgrade;
        var quote = await CreateQuoteAsync(actor, new CreatePlatformBillingQuoteCommand(
            command.BaseOfferingVersionId, command.AddOnOfferingVersionIds, purpose,
            $"{Required(idempotencyKey, "idempotencyKey")}:quote"), cancellationToken);
        return await CreateOrderAsync(actor, new CreatePlatformBillingOrderCommand(quote.Id, idempotencyKey),
            cancellationToken);
    }

    public async Task<PlatformBillingOrderView> RenewSubscriptionAsync(
        TenantBillingActor actor,
        string idempotencyKey,
        CancellationToken cancellationToken = default)
    {
        var subscription = await dbContext.TenantSaasSubscriptions.AsNoTracking()
                               .Where(value => value.TenantId == actor.TenantId)
                               .OrderByDescending(value => value.UpdatedAt)
                               .FirstOrDefaultAsync(cancellationToken)
                           ?? throw Error("Subscription was not found.", "tenant_saas_subscription_not_found");
        var addOns = await dbContext.TenantSaasSubscriptionItems.AsNoTracking()
            .Where(value => value.TenantId == actor.TenantId && value.SubscriptionId == subscription.Id &&
                            value.ItemType == TenantSaasSubscriptionItemType.AddOn &&
                            value.Status == TenantSaasSubscriptionItemStatus.Active)
            .Select(value => value.OfferingVersionId)
            .ToArrayAsync(cancellationToken);
        var quote = await CreateQuoteAsync(actor, new CreatePlatformBillingQuoteCommand(
            subscription.BaseOfferingVersionId, addOns, PlatformBillingOrderPurpose.Renewal,
            $"{Required(idempotencyKey, "idempotencyKey")}:quote"), cancellationToken);
        return await CreateOrderAsync(actor, new CreatePlatformBillingOrderCommand(quote.Id, idempotencyKey),
            cancellationToken);
    }

    public async Task<TenantSubscriptionView> CancelSubscriptionAsync(
        TenantBillingActor actor,
        CancellationToken cancellationToken = default)
    {
        var subscription = await dbContext.TenantSaasSubscriptions
                               .Where(value => value.TenantId == actor.TenantId)
                               .OrderByDescending(value => value.UpdatedAt)
                               .FirstOrDefaultAsync(cancellationToken)
                           ?? throw Error("Subscription was not found.", "tenant_saas_subscription_not_found");
        subscription.CancelAtPeriodEnd = true;
        subscription.CancelledAt = DateTimeOffset.UtcNow;
        await dbContext.SaveChangesAsync(cancellationToken);
        return await ToSubscriptionViewAsync(subscription, cancellationToken);
    }

    public Task<IReadOnlyCollection<FeatureQuotaSnapshot>> GetUsageAsync(TenantBillingActor actor,
        CancellationToken cancellationToken = default)
    {
        return featureAccessService.GetQuotaSummaryAsync(actor.TenantId, cancellationToken);
    }

    public async Task<IReadOnlyCollection<PlatformBillingInvoice>> GetInvoicesAsync(
        TenantBillingActor actor,
        int limit,
        CancellationToken cancellationToken = default)
    {
        return await dbContext.PlatformBillingInvoices.AsNoTracking()
            .Where(value => value.TenantId == actor.TenantId)
            .OrderByDescending(value => value.CreatedAt)
            .Take(Math.Clamp(limit, 1, 200))
            .ToArrayAsync(cancellationToken);
    }

    public async Task<IReadOnlyCollection<TenantReceivableView>> GetReceivablesAsync(
        TenantBillingActor actor,
        int limit,
        CancellationToken cancellationToken = default)
    {
        return await dbContext.PlatformBillingInvoices.AsNoTracking()
            .Where(value => value.TenantId == actor.TenantId)
            .OrderByDescending(value => value.CreatedAt)
            .Take(Math.Clamp(limit, 1, 200))
            .Select(value => new TenantReceivableView(
                value.Id,
                value.OrderId,
                value.InvoiceNo,
                value.Status,
                value.TotalAmountCents,
                value.Currency,
                value.DueDate,
                value.IssuedAt,
                value.PaidAt))
            .ToArrayAsync(cancellationToken);
    }

    public async Task<PlatformBillingOrderView> CancelOrderAsync(
        TenantBillingActor actor,
        string orderNo,
        CancellationToken cancellationToken = default)
    {
        var normalized = Required(orderNo, "orderNo");
        var order = await dbContext.PlatformBillingOrders.SingleOrDefaultAsync(value =>
                value.TenantId == actor.TenantId && value.OrderNo == normalized,
            cancellationToken) ?? throw Error("Order was not found.", "platform_billing_order_not_found");
        if (order.Status != PlatformBillingOrderStatus.PendingPayment)
            throw Error("Only a pending order can be cancelled.", "platform_billing_order_not_cancellable");
        order.Status = PlatformBillingOrderStatus.Cancelled;
        order.CancelledAt = DateTimeOffset.UtcNow;
        var receivables = await dbContext.PlatformBillingInvoices
            .Where(value => value.TenantId == actor.TenantId && value.OrderId == order.Id &&
                            (value.Status == PlatformBillingInvoiceStatus.Draft ||
                             value.Status == PlatformBillingInvoiceStatus.Issued))
            .ToArrayAsync(cancellationToken);
        foreach (var receivable in receivables) receivable.Status = PlatformBillingInvoiceStatus.Void;
        dbContext.AuditLogs.Add(new AuditLog
        {
            TenantId = actor.TenantId,
            ActorUserId = actor.UserId,
            Action = "platform_billing.order.cancelled",
            TargetType = "platform_billing_orders",
            TargetId = order.Id.ToString()
        });
        await dbContext.SaveChangesAsync(cancellationToken);
        return await LoadOrderAsync(actor.TenantId, normalized, cancellationToken);
    }

    public async Task<IReadOnlyCollection<PlatformBillingRefund>> GetRefundsAsync(
        TenantBillingActor actor,
        int limit,
        CancellationToken cancellationToken = default)
    {
        return await dbContext.PlatformBillingRefunds.AsNoTracking()
            .Where(value => value.TenantId == actor.TenantId)
            .OrderByDescending(value => value.CreatedAt)
            .Take(Math.Clamp(limit, 1, 200))
            .ToArrayAsync(cancellationToken);
    }

    private async Task<PlatformBillingQuoteView> LoadQuoteAsync(Guid tenantId, Guid quoteId,
        CancellationToken cancellationToken)
    {
        var quote = await dbContext.PlatformBillingQuotes.AsNoTracking()
            .SingleAsync(value => value.TenantId == tenantId && value.Id == quoteId, cancellationToken);
        var items = await LoadQuoteItemsAsync(tenantId, quote.Id, cancellationToken);
        return new PlatformBillingQuoteView(
            quote.Id,
            quote.QuoteNo,
            quote.Status,
            quote.Purpose,
            quote.OriginalAmountCents,
            quote.DiscountAmountCents,
            quote.TotalAmountCents,
            quote.Currency,
            quote.ExpiresAt,
            items,
            ReadStringArray(quote.FeatureSnapshot),
            ReadLongDictionary(quote.LimitSnapshot));
    }

    private async Task<PlatformBillingOrderView> LoadOrderAsync(Guid tenantId, string orderNo,
        CancellationToken cancellationToken)
    {
        var normalized = Required(orderNo, "orderNo");
        var order = await dbContext.PlatformBillingOrders.AsNoTracking()
                        .SingleOrDefaultAsync(value => value.TenantId == tenantId && value.OrderNo == normalized,
                            cancellationToken)
                    ?? throw Error("Order was not found.", "platform_billing_order_not_found");
        var items = await (
                from item in dbContext.PlatformBillingOrderItems.AsNoTracking()
                join version in dbContext.SaasOfferingVersions.AsNoTracking() on item.OfferingVersionId equals version
                    .Id
                join offering in dbContext.SaasOfferings.AsNoTracking() on version.OfferingId equals offering.Id
                where item.TenantId == tenantId && item.OrderId == order.Id
                orderby item.ItemType, offering.Code
                select new PlatformBillingQuoteItemView(item.OfferingVersionId, offering.Code, offering.Name,
                    item.ItemType, item.UnitAmountCents, item.AmountCents))
            .ToArrayAsync(cancellationToken);
        return new PlatformBillingOrderView(order.Id, order.OrderNo, order.Purpose, order.Status,
            order.TotalAmountCents, order.Currency, order.ExpiresAt, order.PaidAt, items);
    }

    private async Task<PlatformBillingQuoteItemView[]> LoadQuoteItemsAsync(Guid tenantId, Guid quoteId,
        CancellationToken cancellationToken)
    {
        return await (
                from item in dbContext.PlatformBillingQuoteItems.AsNoTracking()
                join version in dbContext.SaasOfferingVersions.AsNoTracking() on item.OfferingVersionId equals version
                    .Id
                join offering in dbContext.SaasOfferings.AsNoTracking() on version.OfferingId equals offering.Id
                where item.TenantId == tenantId && item.QuoteId == quoteId
                orderby item.ItemType, offering.Code
                select new PlatformBillingQuoteItemView(item.OfferingVersionId, offering.Code, offering.Name,
                    item.ItemType, item.UnitAmountCents, item.AmountCents))
            .ToArrayAsync(cancellationToken);
    }

    private async Task<TenantSubscriptionView> ToSubscriptionViewAsync(TenantSaasSubscription subscription,
        CancellationToken cancellationToken)
    {
        var versionIds = await dbContext.TenantSaasSubscriptionItems.AsNoTracking()
            .Where(value => value.TenantId == subscription.TenantId && value.SubscriptionId == subscription.Id &&
                            value.Status == TenantSaasSubscriptionItemStatus.Active)
            .Select(value => value.OfferingVersionId)
            .ToArrayAsync(cancellationToken);
        if (!versionIds.Contains(subscription.BaseOfferingVersionId))
            versionIds = [.. versionIds, subscription.BaseOfferingVersionId];
        var features = await dbContext.SaasOfferingVersionFeatures.AsNoTracking()
            .Where(value => versionIds.Contains(value.OfferingVersionId))
            .Select(value => value.FeatureCode)
            .Distinct()
            .OrderBy(value => value)
            .ToArrayAsync(cancellationToken);
        return new TenantSubscriptionView(
            subscription.Id,
            subscription.Status,
            subscription.StartsAt,
            subscription.CurrentPeriodStart,
            subscription.CurrentPeriodEnd,
            subscription.CancelAtPeriodEnd,
            subscription.BaseOfferingVersionId,
            subscription.ScheduledBaseOfferingVersionId,
            features);
    }

    private async Task<SaasOfferingVersionItem[]> LoadPublishedVersionsAsync(DateTimeOffset now,
        CancellationToken cancellationToken)
    {
        var rows = await (
                from version in dbContext.SaasOfferingVersions.AsNoTracking()
                join offering in dbContext.SaasOfferings.AsNoTracking() on version.OfferingId equals offering.Id
                where version.Status == SaasOfferingVersionStatus.Published &&
                      offering.Status == SaasOfferingStatus.Active &&
                      (version.EffectiveAt == null || version.EffectiveAt <= now)
                orderby offering.SortOrder, offering.Code, version.Version descending
                select new { Version = version, Offering = offering })
            .ToArrayAsync(cancellationToken);
        var latest = rows.GroupBy(value => value.Offering.Id).Select(group => group.First()).ToArray();
        var ids = latest.Select(value => value.Version.Id).ToArray();
        var features = await dbContext.SaasOfferingVersionFeatures.AsNoTracking()
            .Where(value => ids.Contains(value.OfferingVersionId)).ToArrayAsync(cancellationToken);
        var limits = await dbContext.SaasOfferingVersionLimits.AsNoTracking()
            .Where(value => ids.Contains(value.OfferingVersionId)).ToArrayAsync(cancellationToken);
        return latest.Select(row => new SaasOfferingVersionItem(
                row.Version.Id, row.Offering.Id, row.Offering.Code, row.Offering.Name, row.Offering.Type,
                row.Version.Version, row.Version.Status, row.Version.BillingCycle,
                row.Version.OriginalAmountCents, row.Version.AmountCents, row.Version.Currency,
                row.Version.EffectiveAt, row.Version.PublishedAt,
                features.Where(value => value.OfferingVersionId == row.Version.Id).Select(value => value.FeatureCode)
                    .Order(StringComparer.Ordinal).ToArray(),
                limits.Where(value => value.OfferingVersionId == row.Version.Id).ToDictionary(value => value.MetricCode,
                    value => value.LimitValue, StringComparer.Ordinal)))
            .ToArray();
    }

    private string BuildNotifyUrl(string provider)
    {
        var baseUrl = configuration["PlatformBilling:PublicBaseUrl"]?.TrimEnd('/');
        if (string.IsNullOrWhiteSpace(baseUrl))
            throw Error("Platform billing public base URL is not configured.", "platform_billing_public_url_missing");
        return $"{baseUrl}/api/integrations/platform-billing/callbacks/{provider}";
    }

    private static PlatformBillingPaymentView ToPaymentView(PlatformBillingPayment value)
    {
        return new PlatformBillingPaymentView(value.Id, value.PaymentNo, value.Provider, value.Method, value.Status,
            value.AmountCents,
            value.ClientPayload);
    }

    private static string Number(string prefix)
    {
        return $"{prefix}{DateTimeOffset.UtcNow:yyyyMMddHHmmss}{Guid.NewGuid():N}"[..32];
    }

    private static string Required(string? value, string field)
    {
        return string.IsNullOrWhiteSpace(value) ? throw Error($"{field} is required.", "required_field") : value.Trim();
    }

    private static string NormalizeProvider(string value)
    {
        return Required(value, "provider").ToLowerInvariant().Replace('-', '_');
    }

    private static PlatformBillingException Error(string message, string code)
    {
        return new PlatformBillingException(message, code);
    }

    private static string[] ReadStringArray(JsonElement value)
    {
        return value.ValueKind == JsonValueKind.Array
            ? value.EnumerateArray().Where(item => item.ValueKind == JsonValueKind.String)
                .Select(item => item.GetString()!).ToArray()
            : [];
    }

    private static IReadOnlyDictionary<string, long> ReadLongDictionary(JsonElement value)
    {
        if (value.ValueKind != JsonValueKind.Object) return new Dictionary<string, long>(StringComparer.Ordinal);
        return value.EnumerateObject().Where(property => property.Value.TryGetInt64(out _))
            .ToDictionary(property => property.Name, property => property.Value.GetInt64(), StringComparer.Ordinal);
    }
}