using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Tiku.Application.Commerce;
using Tiku.Domain.Commerce;

namespace Tiku.Infrastructure.Commerce;

public sealed partial class CommerceService
{
    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);
    }
}