using System.Net;
using System.Net.Http.Json;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Tiku.Application.Commerce;
using Tiku.Application.Notifications;
using Tiku.Application.PlatformBilling;
using Tiku.Application.Security;
using Tiku.Domain.Common;
using Tiku.Domain.Operations;
using Tiku.Domain.Platform;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;

namespace Tiku.Infrastructure.PlatformBilling;

internal sealed class CommercialBillingProcessor(
    TikuDbContext directoryDbContext,
    ITenantContext tenantContext,
    ITenantExecutionScope tenantExecutionScope,
    IOptions<CommercialBillingOptions> options,
    HttpClient httpClient) : ICommercialBillingProcessor
{
    private static readonly TimeSpan[] RetryDelays =
    [
        TimeSpan.FromMinutes(1),
        TimeSpan.FromMinutes(5),
        TimeSpan.FromMinutes(30),
        TimeSpan.FromHours(2)
    ];

    public async Task<int> ProcessDueAsync(CancellationToken cancellationToken = default)
    {
        if (!options.Value.Enabled) return 0;
        if (!tenantContext.IsSystem || tenantContext.TenantId.HasValue)
            throw new InvalidOperationException("Commercial billing discovery requires a global system context.");

        var processed = 0;
        processed += await ProcessRenewalsAsync(cancellationToken);
        processed += await GenerateRemindersAsync(cancellationToken);
        processed += await ProcessRefundsAsync(cancellationToken);
        processed += await DispatchDunningAsync(cancellationToken);
        return processed;
    }

    private async Task<int> ProcessRenewalsAsync(CancellationToken cancellationToken)
    {
        var now = DateTimeOffset.UtcNow;
        var candidates = await (
                from subscription in directoryDbContext.TenantSaasSubscriptions.AsNoTracking()
                join policy in directoryDbContext.TenantBillingPolicies.AsNoTracking() on subscription.TenantId equals
                    policy.TenantId
                where policy.AutoGenerateRenewal &&
                      (subscription.Status == TenantSaasSubscriptionStatus.Active ||
                       subscription.Status == TenantSaasSubscriptionStatus.Trial) &&
                      subscription.CurrentPeriodEnd <= now.AddDays(policy.RenewalLeadDays)
                orderby subscription.CurrentPeriodEnd
                select new { subscription.TenantId, SubscriptionId = subscription.Id })
            .Take(BatchSize())
            .ToArrayAsync(cancellationToken);

        foreach (var candidate in candidates)
            await tenantExecutionScope.ExecuteAsync(
                Scope(candidate.TenantId, "Generate subscription renewal receivable", candidate.SubscriptionId),
                async (services, token) =>
                {
                    var db = services.GetRequiredService<TikuDbContext>();
                    var subscription =
                        await db.TenantSaasSubscriptions.SingleAsync(value => value.Id == candidate.SubscriptionId,
                            token);
                    var key = $"auto-renewal:{subscription.Id:N}:{subscription.CurrentPeriodEnd:yyyyMMddHHmmss}";
                    var existingOrder = await db.PlatformBillingOrders.SingleOrDefaultAsync(value =>
                        value.TenantId == subscription.TenantId && value.IdempotencyKey == key, token);
                    PlatformBillingOrderView orderView;
                    if (existingOrder is null)
                    {
                        var ownerId = await db.Tenants.AsNoTracking()
                                          .Where(value => value.Id == subscription.TenantId)
                                          .Select(value => value.OwnerUserId)
                                          .SingleAsync(token)
                                      ?? throw new PlatformBillingException(
                                          "Tenant owner is required for renewal billing.", "tenant_owner_not_found");
                        orderView = await services.GetRequiredService<ITenantBillingService>()
                            .RenewSubscriptionAsync(new TenantBillingActor(ownerId, subscription.TenantId), key, token);
                        existingOrder =
                            await db.PlatformBillingOrders.SingleAsync(value => value.Id == orderView.Id, token);
                        existingOrder.ExpiresAt = subscription.CurrentPeriodEnd;
                    }
                    else
                    {
                        orderView = new PlatformBillingOrderView(
                            existingOrder.Id,
                            existingOrder.OrderNo,
                            existingOrder.Purpose,
                            existingOrder.Status,
                            existingOrder.TotalAmountCents,
                            existingOrder.Currency,
                            existingOrder.ExpiresAt,
                            existingOrder.PaidAt,
                            []);
                    }

                    if (!await db.PlatformBillingInvoices.AnyAsync(value =>
                            value.TenantId == subscription.TenantId && value.OrderId == existingOrder.Id, token))
                        db.PlatformBillingInvoices.Add(new PlatformBillingInvoice
                        {
                            TenantId = subscription.TenantId,
                            OrderId = existingOrder.Id,
                            InvoiceNo = Number("AR"),
                            Status = PlatformBillingInvoiceStatus.Issued,
                            TotalAmountCents = orderView.TotalAmountCents,
                            Currency = orderView.Currency,
                            DueDate = DateOnly.FromDateTime(subscription.CurrentPeriodEnd.UtcDateTime),
                            IssuedAt = now,
                            BillingProfileSnapshot = await BillingProfileSnapshotAsync(db, subscription.TenantId, token)
                        });
                    await db.SaveChangesAsync(token);
                },
                cancellationToken);
        return candidates.Length;
    }

    private async Task<int> GenerateRemindersAsync(CancellationToken cancellationToken)
    {
        var today = DateOnly.FromDateTime(DateTime.UtcNow);
        var candidates = await directoryDbContext.PlatformBillingInvoices.AsNoTracking()
            .Where(value => value.DueDate != null &&
                            (value.Status == PlatformBillingInvoiceStatus.Issued ||
                             value.Status == PlatformBillingInvoiceStatus.Overdue) &&
                            value.DueDate <= today.AddDays(7))
            .OrderBy(value => value.DueDate)
            .Select(value => new { value.TenantId, InvoiceId = value.Id })
            .Take(BatchSize())
            .ToArrayAsync(cancellationToken);
        var created = 0;
        foreach (var candidate in candidates)
            created += await tenantExecutionScope.ExecuteAsync(
                Scope(candidate.TenantId, "Generate billing reminder", candidate.InvoiceId),
                async (services, token) =>
                {
                    var db = services.GetRequiredService<TikuDbContext>();
                    var invoice =
                        await db.PlatformBillingInvoices.SingleAsync(value => value.Id == candidate.InvoiceId, token);
                    var dueDate = invoice.DueDate!.Value;
                    if (dueDate < today && invoice.Status == PlatformBillingInvoiceStatus.Issued)
                        invoice.Status = PlatformBillingInvoiceStatus.Overdue;
                    var schedule = ReminderFor(today, dueDate);
                    if (schedule is null || await db.PlatformBillingInvoiceReminders.AnyAsync(value =>
                            value.TenantId == invoice.TenantId && value.InvoiceId == invoice.Id &&
                            value.ReminderType == schedule.Value.Type &&
                            value.Channel == PlatformBillingInvoiceReminderChannel.Internal &&
                            value.ReminderDate == today, token))
                    {
                        await db.SaveChangesAsync(token);
                        return 0;
                    }

                    var reminder = new PlatformBillingInvoiceReminder
                    {
                        TenantId = invoice.TenantId,
                        InvoiceId = invoice.Id,
                        ReminderType = schedule.Value.Type,
                        Channel = PlatformBillingInvoiceReminderChannel.Internal,
                        Status = PlatformBillingInvoiceReminderStatus.Sent,
                        ReminderDate = today,
                        ReminderLevel = schedule.Value.Level,
                        DueDate = dueDate,
                        BalanceCentsSnapshot = invoice.TotalAmountCents,
                        Message = schedule.Value.Message,
                        SentAt = DateTimeOffset.UtcNow
                    };
                    db.PlatformBillingInvoiceReminders.Add(reminder);
                    var ownerId = await db.Tenants.AsNoTracking().Where(value => value.Id == invoice.TenantId)
                        .Select(value => value.OwnerUserId).SingleAsync(token);
                    if (ownerId.HasValue)
                        await services.GetRequiredService<INotificationProvider>().UpsertInAppAsync(
                            new InAppNotificationRequest(
                                invoice.TenantId,
                                ownerId.Value,
                                "saas_billing_reminder",
                                schedule.Value.Level >= 4 ? NotificationSeverity.Error : NotificationSeverity.Warning,
                                "SaaS 服务费账单提醒",
                                schedule.Value.Message,
                                SourceType: "platform_billing_invoices",
                                SourceId: invoice.Id,
                                DedupeKey: $"billing:{invoice.Id:N}:{schedule.Value.Type}:{today:yyyyMMdd}"),
                            token);
                    var channels = await db.PlatformBillingDunningNotificationChannels.AsNoTracking()
                        .Where(value => value.Enabled && value.MinReminderLevel <= schedule.Value.Level &&
                                        (value.TenantIds.Length == 0 || value.TenantIds.Contains(invoice.TenantId)))
                        .ToArrayAsync(token);
                    foreach (var channel in channels.Where(value =>
                                 value.ReminderTypes.Length == 0 ||
                                 value.ReminderTypes.Contains(schedule.Value.Type.ToString().ToLowerInvariant())))
                        db.PlatformBillingDunningNotificationEvents.Add(new PlatformBillingDunningNotificationEvent
                        {
                            TenantId = invoice.TenantId,
                            ChannelId = channel.Id,
                            ReminderId = reminder.Id,
                            InvoiceId = invoice.Id,
                            Provider = channel.Provider,
                            Status = PlatformBillingDunningNotificationStatus.Pending,
                            ScheduledAt = DateTimeOffset.UtcNow,
                            RequestPayload = JsonSerializer.SerializeToElement(new
                            {
                                invoice.InvoiceNo,
                                invoice.TotalAmountCents,
                                invoice.Currency,
                                invoice.DueDate,
                                reminder.ReminderType,
                                reminder.Message
                            })
                        });
                    await db.SaveChangesAsync(token);
                    return 1;
                },
                cancellationToken);
        return created;
    }

    private async Task<int> ProcessRefundsAsync(CancellationToken cancellationToken)
    {
        var candidates = await directoryDbContext.PlatformBillingRefunds.AsNoTracking()
            .Where(value => value.Status == PlatformBillingRefundStatus.Processing)
            .OrderBy(value => value.UpdatedAt)
            .Select(value => new { value.TenantId, RefundId = value.Id })
            .Take(BatchSize())
            .ToArrayAsync(cancellationToken);
        foreach (var candidate in candidates)
            await tenantExecutionScope.ExecuteAsync(
                Scope(candidate.TenantId, "Execute approved SaaS refund", candidate.RefundId),
                async (services, token) =>
                {
                    var db = services.GetRequiredService<TikuDbContext>();
                    var refund =
                        await db.PlatformBillingRefunds.SingleAsync(value => value.Id == candidate.RefundId, token);
                    var payment =
                        await db.PlatformBillingPayments.SingleAsync(value => value.Id == refund.PaymentId, token);
                    var order = await db.PlatformBillingOrders.SingleAsync(value => value.Id == refund.OrderId, token);
                    try
                    {
                        var result = await services.GetRequiredService<IPlatformBillingPaymentGateway>()
                            .CreateRefundAsync(
                                payment.Provider,
                                new CreateRefundProviderRequest(
                                    refund.TenantId,
                                    order.OrderNo,
                                    refund.RefundNo,
                                    payment.ProviderTradeNo,
                                    refund.AmountCents,
                                    refund.Reason,
                                    JsonSerializer.SerializeToElement(new { refund.SubscriptionEffect })),
                                token);
                        if (!result.Succeeded)
                            throw new PlatformBillingException("Refund provider did not accept the refund.",
                                "platform_billing_refund_provider_failed");
                        refund.Status = PlatformBillingRefundStatus.Succeeded;
                        refund.ProviderRefundNo = result.ProviderRefundNo;
                        refund.CompletedAt = DateTimeOffset.UtcNow;
                        refund.LastError = null;
                        var totalRefunded = await db.PlatformBillingRefunds.AsNoTracking()
                            .Where(value => value.TenantId == refund.TenantId && value.PaymentId == payment.Id &&
                                            value.Status == PlatformBillingRefundStatus.Succeeded &&
                                            value.Id != refund.Id)
                            .SumAsync(value => (int?)value.AmountCents, token) ?? 0;
                        if (totalRefunded + refund.AmountCents >= payment.AmountCents)
                        {
                            payment.Status = PlatformBillingPaymentStatus.Refunded;
                            order.Status = PlatformBillingOrderStatus.Refunded;
                        }

                        await ApplyRefundEffectAsync(db, refund, token);
                        db.AuditLogs.Add(new AuditLog
                        {
                            TenantId = refund.TenantId,
                            ActorUserId = refund.ReviewedBy,
                            Action = "platform.saas.refund.succeeded",
                            TargetType = "platform_billing_refunds",
                            TargetId = refund.Id.ToString(),
                            Details = JsonSerializer.SerializeToElement(new
                                { refund.AmountCents, refund.SubscriptionEffect })
                        });
                    }
                    catch (Exception exception)
                    {
                        refund.Status = PlatformBillingRefundStatus.Failed;
                        refund.LastError = Truncate(exception.Message, 2000);
                    }

                    await db.SaveChangesAsync(token);
                    await services.GetRequiredService<ITenantFeatureCacheInvalidator>()
                        .InvalidateAsync(refund.TenantId, token);
                },
                cancellationToken);
        return candidates.Length;
    }

    private async Task<int> DispatchDunningAsync(CancellationToken cancellationToken)
    {
        var now = DateTimeOffset.UtcNow;
        var candidates = await directoryDbContext.PlatformBillingDunningNotificationEvents.AsNoTracking()
            .Where(value => (value.Status == PlatformBillingDunningNotificationStatus.Pending ||
                             value.Status == PlatformBillingDunningNotificationStatus.Retrying) &&
                            value.Attempts < 5 && value.ScheduledAt <= now &&
                            (value.NextAttemptAt == null || value.NextAttemptAt <= now))
            .OrderBy(value => value.NextAttemptAt).ThenBy(value => value.CreatedAt)
            .Select(value => new { value.TenantId, EventId = value.Id })
            .Take(BatchSize())
            .ToArrayAsync(cancellationToken);
        foreach (var candidate in candidates)
            await tenantExecutionScope.ExecuteAsync(
                Scope(candidate.TenantId, "Dispatch billing dunning notification", candidate.EventId),
                async (services, token) =>
                {
                    var db = services.GetRequiredService<TikuDbContext>();
                    var item = await db.PlatformBillingDunningNotificationEvents.SingleAsync(
                        value => value.Id == candidate.EventId, token);
                    var channel = await db.PlatformBillingDunningNotificationChannels.AsNoTracking()
                        .SingleAsync(value => value.Id == item.ChannelId, token);
                    item.Status = PlatformBillingDunningNotificationStatus.Processing;
                    item.Attempts++;
                    item.LastAttemptAt = DateTimeOffset.UtcNow;
                    try
                    {
                        var uri = await ValidateWebhookAsync(channel.WebhookUrl, token);
                        using var request = new HttpRequestMessage(HttpMethod.Post, uri)
                        {
                            Content = JsonContent.Create(item.RequestPayload)
                        };
                        if (!string.IsNullOrWhiteSpace(channel.SecretRef))
                        {
                            var platformTenantId = await db.Tenants.AsNoTracking()
                                .Where(value => value.Mode == TenantMode.PlatformOwned)
                                .Select(value => value.Id)
                                .SingleAsync(token);
                            var secret = await services.GetRequiredService<ITenantSecretService>()
                                .GetActiveSecretPayloadAsync(platformTenantId, channel.SecretRef, token);
                            var signingSecret = GetSecret(secret, "signingSecret", "secret");
                            var signature = Convert.ToHexString(HMACSHA256.HashData(
                                Encoding.UTF8.GetBytes(signingSecret),
                                Encoding.UTF8.GetBytes(item.RequestPayload.GetRawText()))).ToLowerInvariant();
                            request.Headers.Add("X-Tiku-Signature", $"sha256={signature}");
                        }

                        using var timeout = CancellationTokenSource.CreateLinkedTokenSource(token);
                        timeout.CancelAfter(TimeSpan.FromSeconds(Math.Clamp(channel.TimeoutSeconds, 1, 60)));
                        using var response = await httpClient.SendAsync(request, timeout.Token);
                        var summary = RedactResponseSummary(await response.Content.ReadAsStringAsync(timeout.Token));
                        item.LastHttpCode = (int)response.StatusCode;
                        item.LastResponseSummary = summary;
                        if (!response.IsSuccessStatusCode)
                            throw new HttpRequestException($"Dunning webhook returned {(int)response.StatusCode}.",
                                null, response.StatusCode);
                        item.Status = PlatformBillingDunningNotificationStatus.Sent;
                        item.SentAt = DateTimeOffset.UtcNow;
                        item.NextAttemptAt = null;
                        item.LastError = null;
                    }
                    catch (Exception exception)
                    {
                        item.LastError = Truncate(exception.Message, 2000);
                        if (item.Attempts >= 5)
                        {
                            item.Status = PlatformBillingDunningNotificationStatus.Failed;
                            item.NextAttemptAt = null;
                        }
                        else
                        {
                            item.Status = PlatformBillingDunningNotificationStatus.Retrying;
                            item.NextAttemptAt =
                                DateTimeOffset.UtcNow.Add(
                                    RetryDelays[Math.Min(item.Attempts - 1, RetryDelays.Length - 1)]);
                        }
                    }

                    await db.SaveChangesAsync(token);
                },
                cancellationToken);
        return candidates.Length;
    }

    private async Task ApplyRefundEffectAsync(TikuDbContext db, PlatformBillingRefund refund,
        CancellationToken cancellationToken)
    {
        if (refund.SubscriptionEffect == PlatformBillingRefundSubscriptionEffect.KeepService) return;
        var subscription = await db.TenantSaasSubscriptions
            .OrderByDescending(value => value.UpdatedAt)
            .FirstOrDefaultAsync(value => value.TenantId == refund.TenantId, cancellationToken);
        if (subscription is null) return;
        if (refund.SubscriptionEffect == PlatformBillingRefundSubscriptionEffect.CancelAtPeriodEnd)
        {
            subscription.CancelAtPeriodEnd = true;
            subscription.CancelledAt = DateTimeOffset.UtcNow;
            return;
        }

        subscription.Status = TenantSaasSubscriptionStatus.Cancelled;
        subscription.CancelAtPeriodEnd = false;
        subscription.CancelledAt = DateTimeOffset.UtcNow;
        subscription.LifecycleVersion++;
        var tenant = await db.Tenants.SingleAsync(value => value.Id == refund.TenantId, cancellationToken);
        tenant.BillingStatus = BillingStatus.Cancelled;
        var items = await db.TenantSaasSubscriptionItems
            .Where(value => value.TenantId == refund.TenantId && value.SubscriptionId == subscription.Id &&
                            (value.Status == TenantSaasSubscriptionItemStatus.Active ||
                             value.Status == TenantSaasSubscriptionItemStatus.Scheduled))
            .ToArrayAsync(cancellationToken);
        foreach (var item in items)
        {
            item.Status = TenantSaasSubscriptionItemStatus.Cancelled;
            item.EndsAt = DateTimeOffset.UtcNow > item.StartsAt ? DateTimeOffset.UtcNow : item.StartsAt.AddTicks(1);
        }
    }

    private async Task<Uri> ValidateWebhookAsync(string value, CancellationToken cancellationToken)
    {
        if (!Uri.TryCreate(value, UriKind.Absolute, out var uri) || uri.Scheme != Uri.UriSchemeHttps || uri.IsLoopback)
            throw new PlatformBillingException("Dunning webhook must be a non-loopback HTTPS URL.",
                "dunning_webhook_rejected");
        if (options.Value.AllowedWebhookHosts.Length == 0 ||
            !options.Value.AllowedWebhookHosts.Contains(uri.Host, StringComparer.OrdinalIgnoreCase))
            throw new PlatformBillingException("Dunning webhook host is not allowlisted.", "dunning_webhook_rejected");
        IPAddress[] addresses;
        try
        {
            addresses = IPAddress.TryParse(uri.DnsSafeHost, out var literal)
                ? [literal]
                : await Dns.GetHostAddressesAsync(uri.DnsSafeHost, cancellationToken);
        }
        catch (SocketException)
        {
            throw new PlatformBillingException("Dunning webhook host could not be resolved.",
                "dunning_webhook_rejected");
        }

        if (addresses.Length == 0 || addresses.Any(IsPrivate))
            throw new PlatformBillingException("Dunning webhook cannot resolve to a private or reserved address.",
                "dunning_webhook_rejected");
        return uri;
    }

    private static bool IsPrivate(IPAddress address)
    {
        if (address.IsIPv4MappedToIPv6) address = address.MapToIPv4();
        if (IPAddress.IsLoopback(address) || address.Equals(IPAddress.Any) || address.Equals(IPAddress.IPv6Any) ||
            address.Equals(IPAddress.None) || address.Equals(IPAddress.IPv6None))
            return true;
        var bytes = address.GetAddressBytes();
        if (address.AddressFamily == AddressFamily.InterNetwork)
            return bytes[0] == 0 || bytes[0] == 10 || bytes[0] == 127 || bytes[0] >= 224 ||
                   (bytes[0] == 100 && bytes[1] is >= 64 and <= 127) ||
                   (bytes[0] == 169 && bytes[1] == 254) ||
                   (bytes[0] == 172 && bytes[1] is >= 16 and <= 31) ||
                   (bytes[0] == 192 && bytes[1] == 168) ||
                   (bytes[0] == 198 && bytes[1] is 18 or 19);
        return address.AddressFamily != AddressFamily.InterNetworkV6 ||
               address.IsIPv6LinkLocal || address.IsIPv6Multicast || address.IsIPv6SiteLocal ||
               (bytes[0] & 0xfe) == 0xfc;
    }

    private static (PlatformBillingInvoiceReminderType Type, int Level, string Message)? ReminderFor(DateOnly today,
        DateOnly dueDate)
    {
        var days = dueDate.DayNumber - today.DayNumber;
        return days switch
        {
            7 => (PlatformBillingInvoiceReminderType.DueSoon, 1, "SaaS 服务费账单将在 7 天后到期。"),
            1 => (PlatformBillingInvoiceReminderType.DueSoon, 2, "SaaS 服务费账单将在明日到期。"),
            -1 => (PlatformBillingInvoiceReminderType.Overdue, 3, "SaaS 服务费账单已逾期 1 天。"),
            <= -6 => (PlatformBillingInvoiceReminderType.FinalNotice, 4, "SaaS 服务费账单即将超过宽限期。"),
            _ => null
        };
    }

    private static async Task<JsonElement> BillingProfileSnapshotAsync(TikuDbContext db, Guid tenantId,
        CancellationToken cancellationToken)
    {
        var profile = await db.TenantBillingProfiles.AsNoTracking()
            .SingleOrDefaultAsync(value => value.TenantId == tenantId, cancellationToken);
        return profile is null
            ? JsonDefaults.Object()
            : JsonSerializer.SerializeToElement(new
            {
                profile.BillingName,
                profile.TaxId,
                profile.ContactName,
                profile.ContactPhone,
                profile.ContactEmail,
                profile.InvoiceTitle,
                profile.InvoiceType
            });
    }

    private SystemScopeRequest Scope(Guid tenantId, string reason, Guid correlationId)
    {
        return new SystemScopeRequest(tenantId, SystemScopeCallerType.Worker, nameof(CommercialBillingProcessor),
            reason,
            correlationId.ToString("N"));
    }

    private int BatchSize()
    {
        return Math.Clamp(options.Value.BatchSize, 1, 1000);
    }

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

    private static string Truncate(string value, int length)
    {
        return value.Length <= length ? value : value[..length];
    }

    private static string RedactResponseSummary(string value)
    {
        var redacted = Regex.Replace(
            value,
            "(?i)(\\\"?(?:token|secret|password|authorization|access_token|refresh_token|signature)\\\"?\\s*[:=]\\s*\\\")[^\\\"]*(\\\")",
            "$1****$2",
            RegexOptions.CultureInvariant,
            TimeSpan.FromMilliseconds(100));
        redacted = Regex.Replace(
            redacted,
            "(?i)bearer\\s+[a-z0-9._~+/-]+=*",
            "Bearer ****",
            RegexOptions.CultureInvariant,
            TimeSpan.FromMilliseconds(100));
        redacted = Regex.Replace(
            redacted,
            "(?i)([?&](?:key|token|secret|signature)=)[^&\\s]+",
            "$1****",
            RegexOptions.CultureInvariant,
            TimeSpan.FromMilliseconds(100));
        return Truncate(redacted, 500);
    }

    private static string GetSecret(JsonElement value, params string[] names)
    {
        foreach (var name in names)
            if (value.ValueKind == JsonValueKind.Object && value.TryGetProperty(name, out var property) &&
                property.ValueKind == JsonValueKind.String && !string.IsNullOrWhiteSpace(property.GetString()))
                return property.GetString()!;

        throw new PlatformBillingException("Dunning signing secret is missing.", "dunning_secret_missing");
    }
}