using System.Globalization;
using System.Security.Cryptography;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Tiku.Application.Commerce;
using Tiku.Domain.Commerce;
using Tiku.Domain.Common;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;

namespace Tiku.Infrastructure.Commerce;

public sealed class CommerceService(
    TikuDbContext dbContext,
    IPaymentProviderGateway paymentGateway) : ICommerceService
{
    private sealed record CouponApplication(Coupon Coupon, CouponRedemption Redemption, int DiscountCents);

    public async Task<CommerceOrderItem> CreateOrderAsync(
        CommerceActor actor,
        CreateCommerceOrderCommand command,
        CancellationToken cancellationToken = default)
    {
        if (command.Quantity is < 1 or > 99)
        {
            throw new CommerceException("Quantity must be between 1 and 99.", "invalid_quantity");
        }

        await AssertActiveMemberAsync(actor, cancellationToken);
        var plan = await dbContext.SvipPlans
            .AsNoTracking()
            .SingleOrDefaultAsync(item =>
                item.TenantId == actor.TenantId &&
                item.Id == command.PlanId &&
                item.IsActive,
                cancellationToken)
            ?? throw new CommerceException("SVIP plan was not found.", "svip_plan_not_found");

        if (plan.CouponOnly &&
            string.IsNullOrWhiteSpace(command.CouponCode) &&
            !command.CouponRedemptionId.HasValue)
        {
            throw new CommerceException("This SVIP plan requires a coupon.", "coupon_required");
        }

        if (command.RegionId.HasValue)
        {
            var regionExists = await dbContext.Regions
                .AnyAsync(item => item.TenantId == actor.TenantId && item.Id == command.RegionId.Value, cancellationToken);
            if (!regionExists)
            {
                throw new CommerceException("Region was not found.", "region_not_found");
            }
        }

        await using var transaction = dbContext.Database.IsRelational()
            ? await dbContext.Database.BeginTransactionAsync(cancellationToken)
            : null;
        var originalAmountCents = checked(plan.PriceCents * command.Quantity);
        var coupon = await ApplyCouponForOrderAsync(
            actor,
            command,
            plan,
            originalAmountCents,
            cancellationToken);
        if (plan.CouponOnly && coupon is null)
        {
            throw new CommerceException("This SVIP plan requires a coupon.", "coupon_required");
        }

        var amountCents = Math.Max(0, originalAmountCents - (coupon?.DiscountCents ?? 0));
        var order = new Order
        {
            TenantId = actor.TenantId,
            UserId = actor.UserId,
            PlanId = plan.Id,
            RegionId = command.RegionId ?? plan.RegionId,
            OrderNo = GenerateOrderNo(),
            Status = OrderStatus.Pending,
            ProductType = "svip",
            ProductName = plan.Name,
            AmountCents = amountCents,
            PayMethod = NormalizeMethod(command.PayMethod),
            PayProvider = NormalizeProvider(command.PayProvider),
            Days = checked(plan.Days * command.Quantity),
            RawPayload = JsonSerializer.SerializeToElement(new
            {
                source = "student_checkout",
                command.Quantity,
                requestedCouponCode = command.CouponCode,
                requestedCouponRedemptionId = command.CouponRedemptionId,
                plan.PriceCents,
                plan.OriginalPriceCents,
                originalAmountCents,
                discountCents = coupon?.DiscountCents ?? 0,
                couponId = coupon?.Coupon.Id,
                couponCode = coupon?.Coupon.Code,
                couponRedemptionId = coupon?.Redemption.Id
            })
        };
        dbContext.Orders.Add(order);
        dbContext.OrderItems.Add(new OrderItem
        {
            TenantId = actor.TenantId,
            OrderId = order.Id,
            ItemType = "svip_plan",
            ItemId = plan.Id,
            Name = plan.Name,
            Quantity = command.Quantity,
            UnitAmountCents = plan.PriceCents,
            TotalAmountCents = originalAmountCents,
            Metadata = JsonSerializer.SerializeToElement(new
            {
                plan.Days,
                plan.RegionId,
                plan.VpProductId
            })
        });

        if (coupon is not null)
        {
            coupon.Redemption.Status = CouponRedemptionStatus.Used;
            coupon.Redemption.OrderId = order.Id;
            coupon.Redemption.DiscountAppliedCents = coupon.DiscountCents;
            coupon.Redemption.UsedAt = DateTimeOffset.UtcNow;
        }

        if (amountCents == 0)
        {
            var payment = new Payment
            {
                TenantId = actor.TenantId,
                OrderId = order.Id,
                Provider = "manual",
                Method = "zero_amount",
                Status = PaymentStatus.Pending,
                AmountCents = 0
            };
            dbContext.Payments.Add(payment);
            await MarkPaidAsync(
                actor,
                order,
                payment,
                $"zero-{order.OrderNo}",
                order.RawPayload,
                "zero_amount_paid",
                $"zero-{order.OrderNo}",
                true,
                DateTimeOffset.UtcNow,
                cancellationToken);
        }

        await dbContext.SaveChangesAsync(cancellationToken);
        if (transaction is not null)
        {
            await transaction.CommitAsync(cancellationToken);
        }

        return ToOrderItem(order);
    }

    public async Task<CommerceOrderList> GetOrdersAsync(
        CommerceActor actor,
        CommerceOrderQuery query,
        CancellationToken cancellationToken = default)
    {
        await AssertActiveMemberAsync(actor, cancellationToken);
        var orders = dbContext.Orders.AsNoTracking()
            .Where(item => item.TenantId == actor.TenantId && item.UserId == actor.UserId);
        if (!string.IsNullOrWhiteSpace(query.Status))
        {
            orders = orders.Where(item => item.Status == ParseOrderStatus(query.Status));
        }

        var items = await orders
            .OrderByDescending(item => item.CreatedAt)
            .Take(Math.Clamp(query.Limit ?? 20, 1, 100))
            .ToArrayAsync(cancellationToken);

        return new CommerceOrderList(items.Select(ToOrderItem).ToArray());
    }

    public async Task<CommerceOrderItem> GetOrderAsync(
        CommerceActor actor,
        string orderNo,
        CancellationToken cancellationToken = default)
    {
        await AssertActiveMemberAsync(actor, cancellationToken);
        var order = await FindActorOrderAsync(actor, orderNo, cancellationToken);
        return ToOrderItem(order);
    }

    public async Task<CommercePaymentItem> CreatePaymentAsync(
        CommerceActor actor,
        CreateCommercePaymentCommand command,
        CancellationToken cancellationToken = default)
    {
        await AssertActiveMemberAsync(actor, cancellationToken);
        var order = await FindActorOrderAsync(actor, command.OrderNo, cancellationToken);
        if (order.Status != OrderStatus.Pending)
        {
            throw new CommerceException("Only pending orders can create payments.", "order_status_invalid");
        }

        var provider = NormalizeProvider(command.Provider);
        var method = NormalizeMethod(command.Method);
        var payment = await dbContext.Payments
            .Where(item =>
                item.TenantId == actor.TenantId &&
                item.OrderId == order.Id &&
                item.Provider == provider &&
                item.Status == PaymentStatus.Pending)
            .OrderByDescending(item => item.CreatedAt)
            .FirstOrDefaultAsync(cancellationToken);

        if (payment is null)
        {
            payment = new Payment
            {
                TenantId = actor.TenantId,
                OrderId = order.Id,
                Provider = provider,
                Method = method,
                Status = PaymentStatus.Pending,
                AmountCents = order.AmountCents
            };
            dbContext.Payments.Add(payment);
        }

        var result = await paymentGateway.CreatePaymentAsync(
            provider,
            new CreatePaymentProviderRequest(
                actor.TenantId,
                order.OrderNo,
                order.ProductName ?? order.OrderNo,
                order.AmountCents,
                method,
                command.OpenId,
                command.ReturnUrl,
                command.QuitUrl,
                $"/api/commerce/payments/notify/{provider.Replace("_", "-", StringComparison.Ordinal)}?tenantId={actor.TenantId}",
                JsonSerializer.SerializeToElement(new { order.Id, actor.UserId })),
            cancellationToken);

        payment.Method = result.Method;
        payment.RawPayload = result.RawPayload;
        if (!string.IsNullOrWhiteSpace(result.ProviderTradeNo))
        {
            payment.ProviderTradeNo = result.ProviderTradeNo;
        }

        if (IsPaid(result.Status))
        {
            await MarkPaidAsync(
                actor,
                order,
                payment,
                result.ProviderTradeNo,
                result.RawPayload,
                "payment_paid",
                result.ProviderTradeNo,
                true,
                null,
                cancellationToken);
        }
        else
        {
            dbContext.PaymentEvents.Add(new PaymentEvent
            {
                TenantId = actor.TenantId,
                PaymentId = payment.Id,
                Provider = provider,
                EventType = "payment_created",
                Payload = result.RawPayload
            });
        }

        await dbContext.SaveChangesAsync(cancellationToken);
        return ToPaymentItem(payment, order.OrderNo, result.ClientPayload);
    }

    public async Task<CurrentEntitlementItem> GetCurrentEntitlementAsync(
        CommerceActor actor,
        CancellationToken cancellationToken = default)
    {
        await AssertActiveMemberAsync(actor, cancellationToken);
        var now = DateTimeOffset.UtcNow;
        var entitlement = await dbContext.Entitlements
            .AsNoTracking()
            .Where(item =>
                item.TenantId == actor.TenantId &&
                item.UserId == actor.UserId &&
                item.EntitlementType == "svip" &&
                item.Status == EntitlementStatus.Active &&
                (item.ExpiresAt == null || item.ExpiresAt > now))
            .OrderByDescending(item => item.ExpiresAt)
            .FirstOrDefaultAsync(cancellationToken);

        if (entitlement is null)
        {
            return new CurrentEntitlementItem(false, "svip", null, null, "inactive", null);
        }

        return new CurrentEntitlementItem(
            true,
            entitlement.EntitlementType,
            entitlement.StartsAt,
            entitlement.ExpiresAt,
            entitlement.Status.ToString(),
            entitlement.ExpiresAt.HasValue
                ? Math.Max(0, (int)Math.Ceiling((entitlement.ExpiresAt.Value - now).TotalDays))
                : null);
    }

    public async Task<CommerceCouponItem> ClaimCouponAsync(
        CommerceActor actor,
        ClaimCommerceCouponCommand command,
        CancellationToken cancellationToken = default)
    {
        await AssertActiveMemberAsync(actor, cancellationToken);
        var coupon = await FindCouponByCodeAsync(actor.TenantId, command.CouponCode, cancellationToken);
        ValidateCouponClaimable(coupon, null);

        var existing = await dbContext.CouponRedemptions
            .AsNoTracking()
            .Where(item =>
                item.TenantId == actor.TenantId &&
                item.UserId == actor.UserId &&
                item.CouponId == coupon.Id)
            .OrderByDescending(item => item.CreatedAt)
            .FirstOrDefaultAsync(cancellationToken);
        if (existing is not null)
        {
            return ToCouponItem(coupon, existing, null);
        }

        if (coupon.MaxUses.HasValue && coupon.UsedCount >= coupon.MaxUses.Value)
        {
            throw new CommerceException("Coupon usage limit has been reached.", "coupon_usage_limit_reached");
        }

        var redemption = new CouponRedemption
        {
            TenantId = actor.TenantId,
            CouponId = coupon.Id,
            UserId = actor.UserId,
            PlanId = coupon.PlanId,
            CouponCode = coupon.Code,
            Status = CouponRedemptionStatus.Claimed,
            Source = "student_claim",
            ClaimedAt = DateTimeOffset.UtcNow
        };
        coupon.UsedCount += 1;
        dbContext.CouponRedemptions.Add(redemption);
        await dbContext.SaveChangesAsync(cancellationToken);
        return ToCouponItem(coupon, redemption, null);
    }

    public async Task<CommerceCouponList> GetCouponsAsync(
        CommerceActor actor,
        CommerceCouponQuery query,
        CancellationToken cancellationToken = default)
    {
        await AssertActiveMemberAsync(actor, cancellationToken);
        var redemptions = dbContext.CouponRedemptions
            .AsNoTracking()
            .Where(item => item.TenantId == actor.TenantId && item.UserId == actor.UserId);
        if (!string.IsNullOrWhiteSpace(query.Status))
        {
            redemptions = redemptions.Where(item => item.Status == ParseCouponRedemptionStatus(query.Status));
        }

        var items = await redemptions
            .OrderByDescending(item => item.CreatedAt)
            .Take(Math.Clamp(query.Limit ?? 50, 1, 100))
            .ToArrayAsync(cancellationToken);
        var couponIds = items
            .Where(item => item.CouponId.HasValue)
            .Select(item => item.CouponId!.Value)
            .Distinct()
            .ToArray();
        var coupons = await dbContext.Coupons
            .AsNoTracking()
            .Where(item => item.TenantId == actor.TenantId && couponIds.Contains(item.Id))
            .ToDictionaryAsync(item => item.Id, cancellationToken);

        return new CommerceCouponList(
            items.Select(item =>
            {
                coupons.TryGetValue(item.CouponId ?? Guid.Empty, out var coupon);
                return ToCouponItem(coupon, item, item.DiscountAppliedCents);
            }).ToArray());
    }

    public async Task<CommerceCouponCheckResult> CheckCouponAsync(
        CommerceActor actor,
        CheckCommerceCouponCommand command,
        CancellationToken cancellationToken = default)
    {
        await AssertActiveMemberAsync(actor, cancellationToken);
        if (command.Quantity is < 1 or > 99)
        {
            throw new CommerceException("Quantity must be between 1 and 99.", "invalid_quantity");
        }

        var plan = await dbContext.SvipPlans
            .AsNoTracking()
            .SingleOrDefaultAsync(item =>
                item.TenantId == actor.TenantId &&
                item.Id == command.PlanId &&
                item.IsActive,
                cancellationToken)
            ?? throw new CommerceException("SVIP plan was not found.", "svip_plan_not_found");
        var originalAmountCents = checked(plan.PriceCents * command.Quantity);
        try
        {
            var coupon = await ResolveCouponForCheckAsync(actor, command, plan, originalAmountCents, cancellationToken);
            return new CommerceCouponCheckResult(
                true,
                null,
                coupon.Coupon.Id,
                coupon.Redemption.Id,
                coupon.Coupon.Code,
                originalAmountCents,
                coupon.DiscountCents,
                Math.Max(0, originalAmountCents - coupon.DiscountCents),
                FormatCny(Math.Max(0, originalAmountCents - coupon.DiscountCents)));
        }
        catch (CommerceException exception) when (exception.Code.StartsWith("coupon_", StringComparison.Ordinal))
        {
            return new CommerceCouponCheckResult(
                false,
                exception.Code,
                null,
                command.CouponRedemptionId,
                command.CouponCode,
                originalAmountCents,
                0,
                originalAmountCents,
                FormatCny(originalAmountCents));
        }
    }

    public async Task<PaymentNotificationProcessResult> ProcessPaymentNotificationAsync(
        Guid tenantId,
        string provider,
        IReadOnlyDictionary<string, string> headers,
        string rawBody,
        JsonElement body,
        CancellationToken cancellationToken = default)
    {
        var normalizedProvider = NormalizeProvider(provider);
        var notification = await paymentGateway.ParsePaymentNotificationAsync(
            normalizedProvider,
            new PaymentNotificationRequest(
                tenantId,
                normalizedProvider,
                headers,
                rawBody,
                body),
            cancellationToken);

        if (!notification.SignatureValid)
        {
            throw new CommerceException("Payment notification signature is invalid.", "payment_signature_invalid");
        }

        var alreadyProcessed = await dbContext.PaymentEvents.AnyAsync(
            item =>
                item.Provider == normalizedProvider &&
                item.EventId == notification.EventId &&
                item.ProcessedAt != null,
            cancellationToken);
        if (alreadyProcessed)
        {
            return new PaymentNotificationProcessResult(
                normalizedProvider,
                notification.EventId,
                notification.OrderNo,
                "processed",
                true);
        }

        var order = await dbContext.Orders
            .SingleOrDefaultAsync(item =>
                item.TenantId == tenantId &&
                item.OrderNo == notification.OrderNo,
                cancellationToken)
            ?? throw new CommerceException("Order was not found.", "order_not_found");

        if (order.UserId is null)
        {
            throw new CommerceException("Order does not belong to a user.", "order_user_missing");
        }

        if (order.AmountCents != notification.AmountCents)
        {
            dbContext.PaymentEvents.Add(new PaymentEvent
            {
                TenantId = tenantId,
                Provider = normalizedProvider,
                EventType = notification.EventType,
                EventId = notification.EventId,
                SignatureValid = true,
                Payload = notification.RawPayload,
                Error = "payment_amount_mismatch"
            });
            await dbContext.SaveChangesAsync(cancellationToken);
            throw new CommerceException("Payment amount does not match order amount.", "payment_amount_mismatch");
        }

        var payment = await dbContext.Payments
            .Where(item =>
                item.TenantId == tenantId &&
                item.OrderId == order.Id &&
                item.Provider == normalizedProvider)
            .OrderByDescending(item => item.CreatedAt)
            .FirstOrDefaultAsync(cancellationToken);
        if (payment is null)
        {
            payment = new Payment
            {
                TenantId = tenantId,
                OrderId = order.Id,
                Provider = normalizedProvider,
                Method = order.PayMethod,
                Status = PaymentStatus.Pending,
                AmountCents = order.AmountCents
            };
            dbContext.Payments.Add(payment);
        }

        if (notification.Paid && order.Status == OrderStatus.Pending)
        {
            await MarkPaidAsync(
                new CommerceActor(tenantId, order.UserId.Value),
                order,
                payment,
                notification.ProviderTradeNo,
                notification.RawPayload,
                notification.EventType,
                notification.EventId,
                notification.SignatureValid,
                notification.PaidAt,
                cancellationToken);
        }
        else
        {
            dbContext.PaymentEvents.Add(new PaymentEvent
            {
                TenantId = tenantId,
                PaymentId = payment.Id,
                Provider = normalizedProvider,
                EventType = notification.EventType,
                EventId = notification.EventId,
                SignatureValid = notification.SignatureValid,
                Payload = notification.RawPayload,
                ProcessedAt = DateTimeOffset.UtcNow
            });
        }

        await dbContext.SaveChangesAsync(cancellationToken);
        return new PaymentNotificationProcessResult(
            normalizedProvider,
            notification.EventId,
            notification.OrderNo,
            "processed",
            false);
    }

    private async Task MarkPaidAsync(
        CommerceActor actor,
        Order order,
        Payment payment,
        string? providerTradeNo,
        JsonElement rawPayload,
        string eventType,
        string? eventId,
        bool signatureValid,
        DateTimeOffset? paidAtOverride,
        CancellationToken cancellationToken)
    {
        var paidAt = paidAtOverride ?? DateTimeOffset.UtcNow;
        payment.Status = PaymentStatus.Paid;
        payment.ProviderTradeNo = providerTradeNo ?? payment.ProviderTradeNo;
        payment.PaidAt = paidAt;
        order.Status = OrderStatus.Paid;
        order.TradeNo = payment.ProviderTradeNo;
        order.PaidAt = paidAt;

        var days = Math.Max(order.Days ?? 0, 0);
        var current = await dbContext.Entitlements
            .Where(item =>
                item.TenantId == actor.TenantId &&
                item.UserId == actor.UserId &&
                item.EntitlementType == "svip" &&
                item.Status == EntitlementStatus.Active)
            .OrderByDescending(item => item.ExpiresAt)
            .FirstOrDefaultAsync(cancellationToken);
        if (current is null)
        {
            dbContext.Entitlements.Add(new Entitlement
            {
                TenantId = actor.TenantId,
                UserId = actor.UserId,
                EntitlementType = "svip",
                ScopeType = EntitlementScopeType.Tenant,
                SourceType = "order",
                SourceId = order.Id,
                StartsAt = paidAt,
                ExpiresAt = days > 0 ? paidAt.AddDays(days) : null,
                Status = EntitlementStatus.Active,
                Metadata = rawPayload
            });
        }
        else if (days > 0)
        {
            var baseAt = current.ExpiresAt.HasValue && current.ExpiresAt > paidAt
                ? current.ExpiresAt.Value
                : paidAt;
            current.ExpiresAt = baseAt.AddDays(days);
            current.Metadata = rawPayload;
        }

        dbContext.PaymentEvents.Add(new PaymentEvent
        {
            TenantId = actor.TenantId,
            PaymentId = payment.Id,
            Provider = payment.Provider,
            EventType = eventType,
            EventId = eventId,
            SignatureValid = signatureValid,
            Payload = rawPayload,
            ProcessedAt = paidAt
        });
    }

    private async Task<CouponApplication?> ApplyCouponForOrderAsync(
        CommerceActor actor,
        CreateCommerceOrderCommand command,
        SvipPlan plan,
        int originalAmountCents,
        CancellationToken cancellationToken)
    {
        if (command.CouponRedemptionId is null && string.IsNullOrWhiteSpace(command.CouponCode))
        {
            return null;
        }

        var coupon = command.CouponRedemptionId.HasValue
            ? await ResolveCouponByRedemptionAsync(actor, command.CouponRedemptionId.Value, cancellationToken)
            : await ResolveOrClaimCouponByCodeAsync(actor, command.CouponCode, cancellationToken);
        ValidateCouponUsable(coupon.Coupon, coupon.Redemption, plan, command.RegionId);
        return coupon with { DiscountCents = CalculateDiscountCents(coupon.Coupon, originalAmountCents) };
    }

    private async Task<CouponApplication> ResolveCouponForCheckAsync(
        CommerceActor actor,
        CheckCommerceCouponCommand command,
        SvipPlan plan,
        int originalAmountCents,
        CancellationToken cancellationToken)
    {
        if (command.CouponRedemptionId is null && string.IsNullOrWhiteSpace(command.CouponCode))
        {
            throw new CommerceException("Coupon code or redemption id is required.", "coupon_required");
        }

        var coupon = command.CouponRedemptionId.HasValue
            ? await ResolveCouponByRedemptionAsync(actor, command.CouponRedemptionId.Value, cancellationToken)
            : await ResolveCouponByCodeForCheckAsync(actor, command.CouponCode, cancellationToken);
        ValidateCouponUsable(coupon.Coupon, coupon.Redemption, plan, command.RegionId);
        return coupon with { DiscountCents = CalculateDiscountCents(coupon.Coupon, originalAmountCents) };
    }

    private async Task<CouponApplication> ResolveOrClaimCouponByCodeAsync(
        CommerceActor actor,
        string? couponCode,
        CancellationToken cancellationToken)
    {
        var coupon = await FindCouponByCodeAsync(actor.TenantId, couponCode, cancellationToken);
        ValidateCouponClaimable(coupon, null);
        var existing = await dbContext.CouponRedemptions
            .Where(item =>
                item.TenantId == actor.TenantId &&
                item.UserId == actor.UserId &&
                item.CouponId == coupon.Id)
            .OrderByDescending(item => item.CreatedAt)
            .FirstOrDefaultAsync(cancellationToken);
        if (existing is not null)
        {
            return new CouponApplication(coupon, existing, 0);
        }

        if (coupon.MaxUses.HasValue && coupon.UsedCount >= coupon.MaxUses.Value)
        {
            throw new CommerceException("Coupon usage limit has been reached.", "coupon_usage_limit_reached");
        }

        var redemption = new CouponRedemption
        {
            TenantId = actor.TenantId,
            CouponId = coupon.Id,
            UserId = actor.UserId,
            PlanId = coupon.PlanId,
            CouponCode = coupon.Code,
            Status = CouponRedemptionStatus.Claimed,
            Source = "checkout_claim",
            ClaimedAt = DateTimeOffset.UtcNow
        };
        coupon.UsedCount += 1;
        dbContext.CouponRedemptions.Add(redemption);
        return new CouponApplication(coupon, redemption, 0);
    }

    private async Task<CouponApplication> ResolveCouponByCodeForCheckAsync(
        CommerceActor actor,
        string? couponCode,
        CancellationToken cancellationToken)
    {
        var coupon = await FindCouponByCodeAsync(actor.TenantId, couponCode, cancellationToken);
        var redemption = await dbContext.CouponRedemptions
            .AsNoTracking()
            .Where(item =>
                item.TenantId == actor.TenantId &&
                item.UserId == actor.UserId &&
                item.CouponId == coupon.Id)
            .OrderByDescending(item => item.CreatedAt)
            .FirstOrDefaultAsync(cancellationToken);
        if (redemption is not null)
        {
            return new CouponApplication(coupon, redemption, 0);
        }

        ValidateCouponClaimable(coupon, null);
        return new CouponApplication(
            coupon,
            new CouponRedemption
            {
                Id = Guid.Empty,
                TenantId = actor.TenantId,
                CouponId = coupon.Id,
                UserId = actor.UserId,
                PlanId = coupon.PlanId,
                CouponCode = coupon.Code,
                Status = CouponRedemptionStatus.Claimed
            },
            0);
    }

    private async Task<CouponApplication> ResolveCouponByRedemptionAsync(
        CommerceActor actor,
        Guid couponRedemptionId,
        CancellationToken cancellationToken)
    {
        var redemption = await dbContext.CouponRedemptions
            .SingleOrDefaultAsync(item =>
                item.TenantId == actor.TenantId &&
                item.UserId == actor.UserId &&
                item.Id == couponRedemptionId,
                cancellationToken)
            ?? throw new CommerceException("Coupon redemption was not found.", "coupon_redemption_not_found");
        if (redemption.CouponId is null)
        {
            throw new CommerceException("Coupon redemption is not linked to a coupon.", "coupon_redemption_invalid");
        }

        var coupon = await dbContext.Coupons
            .SingleOrDefaultAsync(item => item.TenantId == actor.TenantId && item.Id == redemption.CouponId.Value, cancellationToken)
            ?? throw new CommerceException("Coupon was not found.", "coupon_not_found");
        return new CouponApplication(coupon, redemption, 0);
    }

    private async Task<Coupon> FindCouponByCodeAsync(
        Guid tenantId,
        string? couponCode,
        CancellationToken cancellationToken)
    {
        var code = NormalizeRequired(couponCode, "coupon_code_required");
        return await dbContext.Coupons
            .SingleOrDefaultAsync(item => item.TenantId == tenantId && item.Code == code, cancellationToken)
            ?? throw new CommerceException("Coupon was not found.", "coupon_not_found");
    }

    private static void ValidateCouponClaimable(Coupon coupon, CouponRedemption? redemption)
    {
        var now = DateTimeOffset.UtcNow;
        if (coupon.ValidFrom is not null && coupon.ValidFrom > now ||
            coupon.ValidTo is not null && coupon.ValidTo <= now)
        {
            throw new CommerceException("Coupon is expired or not started.", "coupon_inactive");
        }

        if (redemption is null && coupon.MaxUses.HasValue && coupon.UsedCount >= coupon.MaxUses.Value)
        {
            throw new CommerceException("Coupon usage limit has been reached.", "coupon_usage_limit_reached");
        }
    }

    private static void ValidateCouponUsable(
        Coupon coupon,
        CouponRedemption redemption,
        SvipPlan plan,
        Guid? regionId)
    {
        ValidateCouponClaimable(coupon, redemption);
        if (redemption.Status != CouponRedemptionStatus.Claimed)
        {
            throw new CommerceException("Coupon redemption is not claimable.", "coupon_redemption_status_invalid");
        }

        if (coupon.PlanId.HasValue && coupon.PlanId != plan.Id ||
            redemption.PlanId.HasValue && redemption.PlanId != plan.Id)
        {
            throw new CommerceException("Coupon is not applicable to this plan.", "coupon_plan_not_applicable");
        }

        if (redemption.RegionId.HasValue &&
            regionId.HasValue &&
            redemption.RegionId != regionId)
        {
            throw new CommerceException("Coupon is not applicable to this region.", "coupon_region_not_applicable");
        }
    }

    private static int CalculateDiscountCents(Coupon coupon, int originalAmountCents)
    {
        var discount = coupon.DiscountType switch
        {
            DiscountType.Fixed => (int)Math.Round((coupon.DiscountValue ?? 0) * 100, MidpointRounding.AwayFromZero),
            DiscountType.Percent => (int)Math.Round(
                originalAmountCents * PercentFactor(coupon.DiscountValue ?? 0),
                MidpointRounding.AwayFromZero),
            _ => 0
        };
        return Math.Clamp(discount, 0, originalAmountCents);
    }

    private static decimal PercentFactor(decimal value)
    {
        if (value <= 0)
        {
            return 0;
        }

        return value <= 1 ? value : value / 100;
    }

    private async Task AssertActiveMemberAsync(CommerceActor actor, CancellationToken cancellationToken)
    {
        var exists = await dbContext.TenantMemberships.AnyAsync(item =>
            item.TenantId == actor.TenantId &&
            item.UserId == actor.UserId &&
            item.Status == MembershipStatus.Active,
            cancellationToken);
        if (!exists)
        {
            throw new CommerceException("Current user is not a member of the tenant.", "tenant_access_denied");
        }
    }

    private async Task<Order> FindActorOrderAsync(
        CommerceActor actor,
        string orderNo,
        CancellationToken cancellationToken)
    {
        var trimmed = orderNo.Trim();
        return await dbContext.Orders
            .SingleOrDefaultAsync(item =>
                item.TenantId == actor.TenantId &&
                item.UserId == actor.UserId &&
                item.OrderNo == trimmed,
                cancellationToken)
            ?? throw new CommerceException("Order was not found.", "order_not_found");
    }

    private static CommerceOrderItem ToOrderItem(Order order)
    {
        return new CommerceOrderItem(
            order.Id,
            order.OrderNo,
            order.Status.ToString(),
            order.PlanId,
            order.RegionId,
            order.ProductType,
            order.ProductName,
            order.AmountCents,
            FormatCny(order.AmountCents),
            order.PayMethod,
            order.PayProvider,
            order.TradeNo,
            order.Days,
            order.PaidAt,
            order.CreatedAt,
            order.RawPayload);
    }

    private static CommercePaymentItem ToPaymentItem(Payment payment, string orderNo, JsonElement clientPayload)
    {
        return new CommercePaymentItem(
            payment.Id,
            payment.OrderId,
            orderNo,
            payment.Provider,
            payment.Method,
            payment.Status.ToString(),
            payment.AmountCents,
            FormatCny(payment.AmountCents),
            payment.ProviderTradeNo,
            payment.PaidAt,
            clientPayload,
            payment.RawPayload);
    }

    private static CommerceCouponItem ToCouponItem(
        Coupon? coupon,
        CouponRedemption redemption,
        int? discountPreviewCents)
    {
        return new CommerceCouponItem(
            redemption.Id,
            redemption.CouponId,
            redemption.CouponCode ?? coupon?.Code ?? string.Empty,
            redemption.Status.ToString(),
            redemption.PlanId ?? coupon?.PlanId,
            redemption.RegionId,
            coupon?.DiscountType?.ToString(),
            coupon?.DiscountValue,
            discountPreviewCents,
            discountPreviewCents.HasValue ? FormatCny(discountPreviewCents.Value) : null,
            coupon?.ValidFrom,
            coupon?.ValidTo,
            redemption.ClaimedAt,
            redemption.UsedAt);
    }

    private static OrderStatus ParseOrderStatus(string? status)
    {
        return Enum.TryParse<OrderStatus>(NormalizeEnum(status), true, out var parsed)
            ? parsed
            : throw new CommerceException("Order status is invalid.", "invalid_order_status");
    }

    private static CouponRedemptionStatus ParseCouponRedemptionStatus(string? status)
    {
        return Enum.TryParse<CouponRedemptionStatus>(NormalizeEnum(status), true, out var parsed)
            ? parsed
            : throw new CommerceException("Coupon status is invalid.", "invalid_coupon_status");
    }

    private static string NormalizeEnum(string? value) =>
        string.Concat((value ?? string.Empty).Split(
            ['_', '-', ' '],
            StringSplitOptions.RemoveEmptyEntries));

    private static string NormalizeRequired(string? value, string code)
    {
        var trimmed = value?.Trim();
        return !string.IsNullOrWhiteSpace(trimmed)
            ? trimmed
            : throw new CommerceException("Required commerce value is missing.", code);
    }

    private static string NormalizeProvider(string? provider)
    {
        var normalized = (provider ?? PaymentProviders.Manual)
            .Trim()
            .ToLowerInvariant()
            .Replace("-", "_", StringComparison.Ordinal);

        return normalized switch
        {
            "" => PaymentProviders.Manual,
            "wechat" or "wechatpay" or "wxpay" or "wx_pay" => PaymentProviders.WechatPay,
            "ali_pay" => PaymentProviders.Alipay,
            PaymentProviders.WechatPay or PaymentProviders.Alipay or PaymentProviders.Manual => normalized,
            _ => throw new CommerceException("Payment provider is invalid.", "invalid_payment_provider")
        };
    }

    private static string NormalizeMethod(string? method)
    {
        var normalized = (method ?? "manual").Trim().ToLowerInvariant();
        return string.IsNullOrWhiteSpace(normalized) ? "manual" : normalized;
    }

    private static bool IsPaid(string status) =>
        string.Equals(status, "paid", StringComparison.OrdinalIgnoreCase) ||
        string.Equals(status, "success", StringComparison.OrdinalIgnoreCase) ||
        string.Equals(status, "succeeded", StringComparison.OrdinalIgnoreCase);

    private static string GenerateOrderNo()
    {
        Span<byte> bytes = stackalloc byte[4];
        RandomNumberGenerator.Fill(bytes);
        return $"TK{DateTimeOffset.UtcNow:yyyyMMddHHmmss}{Convert.ToHexString(bytes)}";
    }

    private static string FormatCny(int cents) =>
        (cents / 100m).ToString("0.00", CultureInfo.InvariantCulture);
}
