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.Tenancy;

namespace Tiku.Infrastructure.Commerce;

public sealed partial class CommerceService
{
    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)
    {
        return 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)
    {
        return 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)
    {
        return (cents / 100m).ToString("0.00", CultureInfo.InvariantCulture);
    }
}