using System.Security.Cryptography;
using Microsoft.EntityFrameworkCore;
using Tiku.Application.Commerce;
using Tiku.Domain.Commerce;
using Tiku.Infrastructure.Security;

namespace Tiku.Infrastructure.Commerce;

internal sealed partial class CommerceAdminService
{
    public async Task<TenantRefundList> GetRefundsAsync(
        CommerceAdminActor actor,
        CommerceAdminQuery query,
        CancellationToken cancellationToken = default)
    {
        await AssertAdminAsync(actor, cancellationToken);
        var scope = await RequireDataScopeAsync(actor, cancellationToken);
        var regionIds = scope.RegionIds.ToArray();
        var refunds = dbContext.CommerceRefundRequests.AsNoTracking()
            .Where(item => item.TenantId == actor.TenantId)
            .ApplyDataScope(
                scope,
                item => item.RequestedBy == actor.UserId || dbContext.Orders.Any(order =>
                    order.TenantId == actor.TenantId && order.Id == item.OrderId && order.UserId == actor.UserId),
                item => dbContext.Orders.Any(order =>
                    order.TenantId == actor.TenantId &&
                    order.Id == item.OrderId &&
                    order.RegionId.HasValue &&
                    regionIds.Contains(order.RegionId.Value)));
        if (!string.IsNullOrWhiteSpace(query.Status))
            refunds = refunds.Where(item => item.Status == ParseRefundStatus(query.Status));

        var items = await refunds
            .OrderByDescending(item => item.CreatedAt)
            .Take(Math.Clamp(query.Limit ?? 50, 1, 200))
            .ToArrayAsync(cancellationToken);
        return new TenantRefundList(items);
    }

    public async Task<CommerceRefundRequest> CreateRefundRequestAsync(
        CommerceAdminActor actor,
        CreateRefundRequestCommand command,
        CancellationToken cancellationToken = default)
    {
        await AssertAdminAsync(actor, cancellationToken);
        var scope = await RequireDataScopeAsync(actor, cancellationToken);
        var regionIds = scope.RegionIds.ToArray();
        var order = await dbContext.Orders
                        .Where(item => item.TenantId == actor.TenantId && item.Id == command.OrderId)
                        .ApplyDataScope(
                            scope,
                            item => item.UserId == actor.UserId,
                            item => item.RegionId.HasValue && regionIds.Contains(item.RegionId.Value))
                        .SingleOrDefaultAsync(cancellationToken)
                    ?? throw new CommerceException("Order was not found.", "order_not_found");
        if (order.Status is not (OrderStatus.Paid or OrderStatus.PartiallyRefunded))
            throw new CommerceException("Only paid orders can be refunded.", "order_not_refundable");

        if (command.AmountCents <= 0 || command.AmountCents > order.AmountCents - order.RefundedAmountCents)
            throw new CommerceException("Refund amount is invalid.", "invalid_refund_amount");

        if (command.PaymentId.HasValue)
        {
            var paymentExists = await dbContext.Payments.AnyAsync(
                item => item.TenantId == actor.TenantId && item.Id == command.PaymentId.Value &&
                        item.OrderId == order.Id,
                cancellationToken);
            if (!paymentExists) throw new CommerceException("Payment was not found.", "payment_not_found");
        }

        var refund = new CommerceRefundRequest
        {
            TenantId = actor.TenantId,
            OrderId = order.Id,
            PaymentId = command.PaymentId,
            RequestedBy = actor.UserId,
            RefundNo = $"RF{DateTimeOffset.UtcNow:yyyyMMddHHmmss}{RandomNumberGenerator.GetInt32(1000, 9999)}",
            Provider = order.PayProvider,
            Status = CommerceRefundStatus.Requested,
            AmountCents = command.AmountCents,
            Reason = command.Reason?.Trim(),
            EntitlementAction = command.EntitlementAction,
            Metadata = JsonObjectOrDefault(command.Metadata)
        };
        dbContext.CommerceRefundRequests.Add(refund);
        AddRefundEvent(refund, null, CommerceRefundStatus.Requested, "created", actor.UserId,
            new { refund.AmountCents, refund.Reason });
        await AddAuditAsync(actor, "commerce.refund.created", "commerce_refund_requests", refund.Id,
            new { refund.RefundNo, refund.AmountCents }, cancellationToken);
        await dbContext.SaveChangesAsync(cancellationToken);
        return refund;
    }

    public async Task<CommerceRefundRequest> UpdateRefundStatusAsync(
        CommerceAdminActor actor,
        UpdateRefundStatusCommand command,
        CancellationToken cancellationToken = default)
    {
        await AssertAdminAsync(actor, cancellationToken);
        var scope = await RequireDataScopeAsync(actor, cancellationToken);
        var regionIds = scope.RegionIds.ToArray();
        var refund = await dbContext.CommerceRefundRequests
                         .Where(item => item.TenantId == actor.TenantId && item.Id == command.RefundRequestId)
                         .ApplyDataScope(
                             scope,
                             item => item.RequestedBy == actor.UserId || dbContext.Orders.Any(order =>
                                 order.TenantId == actor.TenantId && order.Id == item.OrderId &&
                                 order.UserId == actor.UserId),
                             item => dbContext.Orders.Any(order =>
                                 order.TenantId == actor.TenantId &&
                                 order.Id == item.OrderId &&
                                 order.RegionId.HasValue &&
                                 regionIds.Contains(order.RegionId.Value)))
                         .SingleOrDefaultAsync(cancellationToken)
                     ?? throw new CommerceException("Refund request was not found.", "refund_not_found");
        var fromStatus = refund.Status;
        if (!IsAllowedRefundTransition(fromStatus, command.Status))
            throw new CommerceException("Refund status transition is invalid.", "invalid_refund_transition");

        refund.Status = command.Status;
        refund.ProviderRefundNo = string.IsNullOrWhiteSpace(command.ProviderRefundNo)
            ? refund.ProviderRefundNo
            : command.ProviderRefundNo.Trim();
        switch (command.Status)
        {
            case CommerceRefundStatus.Approved:
                refund.ReviewedBy = actor.UserId;
                refund.ReviewedAt = DateTimeOffset.UtcNow;
                break;
            case CommerceRefundStatus.Processing:
                refund.ProcessedBy = actor.UserId;
                refund.ProcessedAt = DateTimeOffset.UtcNow;
                break;
            case CommerceRefundStatus.Succeeded:
                refund.SucceededAt = DateTimeOffset.UtcNow;
                await ApplyRefundToOrderAsync(refund, cancellationToken);
                break;
            case CommerceRefundStatus.Failed:
                refund.FailedAt = DateTimeOffset.UtcNow;
                refund.FailureReason = command.Reason;
                break;
            case CommerceRefundStatus.Cancelled or CommerceRefundStatus.Rejected:
                refund.CancelledAt = DateTimeOffset.UtcNow;
                break;
        }

        AddRefundEvent(refund, fromStatus, command.Status, "status_changed", actor.UserId,
            new { command.Reason, command.ProviderRefundNo });
        await AddAuditAsync(actor, "commerce.refund.status_changed", "commerce_refund_requests", refund.Id,
            new { refund.RefundNo, From = fromStatus, To = command.Status }, cancellationToken);
        await dbContext.SaveChangesAsync(cancellationToken);
        return refund;
    }

    public async Task<TenantRefundEventList> GetRefundEventsAsync(
        CommerceAdminActor actor,
        Guid refundRequestId,
        CancellationToken cancellationToken = default)
    {
        await AssertAdminAsync(actor, cancellationToken);
        var scope = await RequireDataScopeAsync(actor, cancellationToken);
        var regionIds = scope.RegionIds.ToArray();
        var refundExists = await dbContext.CommerceRefundRequests
            .Where(item => item.TenantId == actor.TenantId && item.Id == refundRequestId)
            .ApplyDataScope(
                scope,
                item => item.RequestedBy == actor.UserId || dbContext.Orders.Any(order =>
                    order.TenantId == actor.TenantId && order.Id == item.OrderId && order.UserId == actor.UserId),
                item => dbContext.Orders.Any(order =>
                    order.TenantId == actor.TenantId &&
                    order.Id == item.OrderId &&
                    order.RegionId.HasValue &&
                    regionIds.Contains(order.RegionId.Value)))
            .AnyAsync(cancellationToken);
        if (!refundExists) throw new CommerceException("Refund request was not found.", "refund_not_found");

        var items = await dbContext.CommerceRefundEvents.AsNoTracking()
            .Where(item => item.TenantId == actor.TenantId && item.RefundRequestId == refundRequestId)
            .OrderBy(item => item.CreatedAt)
            .ToArrayAsync(cancellationToken);
        return new TenantRefundEventList(items);
    }
}