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

namespace Tiku.Infrastructure.Commerce;

internal sealed partial class CommerceAdminService
{
    public async Task<TenantAdjustmentVoucherList> GetAdjustmentVouchersAsync(
        CommerceAdminActor actor,
        CommerceAdminQuery query,
        CancellationToken cancellationToken = default)
    {
        await AssertAdminAsync(actor, cancellationToken);
        var vouchers = dbContext.CommerceAdjustmentVouchers.AsNoTracking()
            .Where(item => item.TenantId == actor.TenantId);
        if (!string.IsNullOrWhiteSpace(query.Status))
            vouchers = vouchers.Where(item => item.Status == ParseAdjustmentVoucherStatus(query.Status));

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

    public async Task<CommerceAdjustmentVoucher> GetAdjustmentVoucherAsync(
        CommerceAdminActor actor,
        Guid voucherId,
        CancellationToken cancellationToken = default)
    {
        await AssertAdminAsync(actor, cancellationToken);
        return await dbContext.CommerceAdjustmentVouchers.AsNoTracking()
                   .SingleOrDefaultAsync(item => item.TenantId == actor.TenantId && item.Id == voucherId,
                       cancellationToken)
               ?? throw new CommerceException("Adjustment voucher was not found.", "adjustment_voucher_not_found");
    }

    public async Task<CommerceAdjustmentVoucher> CreateAdjustmentVoucherAsync(
        CommerceAdminActor actor,
        CreateAdjustmentVoucherCommand command,
        CancellationToken cancellationToken = default)
    {
        await AssertAdminAsync(actor, cancellationToken);
        ArgumentException.ThrowIfNullOrWhiteSpace(command.Reason);
        await AssertOptionalReferenceAsync(dbContext.CommerceReconciliationIssues, actor.TenantId, command.IssueId,
            "reconciliation_issue_not_found", cancellationToken);
        await AssertOptionalReferenceAsync(dbContext.CommerceReconciliationBatches, actor.TenantId, command.BatchId,
            "reconciliation_batch_not_found", cancellationToken);
        await AssertOptionalReferenceAsync(dbContext.CommerceReconciliationItems, actor.TenantId, command.ItemId,
            "reconciliation_item_not_found", cancellationToken);
        await AssertOptionalReferenceAsync(dbContext.Orders, actor.TenantId, command.OrderId, "order_not_found",
            cancellationToken);
        await AssertOptionalReferenceAsync(dbContext.Payments, actor.TenantId, command.PaymentId, "payment_not_found",
            cancellationToken);
        await AssertOptionalReferenceAsync(dbContext.CommerceRefundRequests, actor.TenantId, command.RefundRequestId,
            "refund_not_found", cancellationToken);
        var voucher = new CommerceAdjustmentVoucher
        {
            TenantId = actor.TenantId,
            IssueId = command.IssueId,
            BatchId = command.BatchId,
            ItemId = command.ItemId,
            OrderId = command.OrderId,
            PaymentId = command.PaymentId,
            RefundRequestId = command.RefundRequestId,
            CreatedBy = actor.UserId,
            VoucherNo = $"ADJ{DateTimeOffset.UtcNow:yyyyMMddHHmmss}{Random.Shared.Next(1000, 9999)}",
            Status = CommerceAdjustmentVoucherStatus.Draft,
            Direction = command.Direction,
            AmountCents = command.AmountCents,
            Currency = string.IsNullOrWhiteSpace(command.Currency) ? "CNY" : command.Currency.Trim().ToUpperInvariant(),
            Reason = command.Reason.Trim(),
            ProofAssetKey = string.IsNullOrWhiteSpace(command.ProofAssetKey) ? null : command.ProofAssetKey.Trim(),
            Metadata = JsonObjectOrDefault(command.Metadata)
        };
        dbContext.CommerceAdjustmentVouchers.Add(voucher);
        dbContext.CommerceAdjustmentVoucherEvents.Add(new CommerceAdjustmentVoucherEvent
        {
            TenantId = actor.TenantId,
            VoucherId = voucher.Id,
            ToStatus = voucher.Status,
            ActorUserId = actor.UserId,
            Note = voucher.Reason,
            Details = JsonSerializer.SerializeToElement(new { voucher.Direction, voucher.AmountCents })
        });
        await AddAuditAsync(actor, "commerce.adjustment_voucher.created", "commerce_adjustment_vouchers", voucher.Id,
            new { voucher.VoucherNo, voucher.Direction, voucher.AmountCents }, cancellationToken);
        await dbContext.SaveChangesAsync(cancellationToken);
        return voucher;
    }

    public async Task<CommerceAdjustmentVoucher> UpdateAdjustmentVoucherStatusAsync(
        CommerceAdminActor actor,
        UpdateAdjustmentVoucherStatusCommand command,
        CancellationToken cancellationToken = default)
    {
        await AssertAdminAsync(actor, cancellationToken);
        var voucher = await dbContext.CommerceAdjustmentVouchers.SingleOrDefaultAsync(
                          item => item.TenantId == actor.TenantId && item.Id == command.VoucherId,
                          cancellationToken) ??
                      throw new CommerceException("Adjustment voucher was not found.", "adjustment_voucher_not_found");
        var fromStatus = voucher.Status;
        if (fromStatus != command.Status && !IsAllowedAdjustmentTransition(fromStatus, command.Status))
            throw new CommerceException("Adjustment voucher status transition is invalid.",
                "invalid_adjustment_status_transition");

        voucher.Status = command.Status;
        if (command.Status is CommerceAdjustmentVoucherStatus.Approved or CommerceAdjustmentVoucherStatus.Rejected)
        {
            voucher.ReviewedBy = actor.UserId;
            voucher.ReviewedAt ??= DateTimeOffset.UtcNow;
        }
        else if (command.Status is CommerceAdjustmentVoucherStatus.Closed or CommerceAdjustmentVoucherStatus.Void)
        {
            voucher.ClosedAt ??= DateTimeOffset.UtcNow;
        }

        dbContext.CommerceAdjustmentVoucherEvents.Add(new CommerceAdjustmentVoucherEvent
        {
            TenantId = actor.TenantId,
            VoucherId = voucher.Id,
            FromStatus = fromStatus,
            ToStatus = command.Status,
            ActorUserId = actor.UserId,
            Note = command.Note,
            Details = JsonSerializer.SerializeToElement(new { })
        });
        await AddAuditAsync(actor, "commerce.adjustment_voucher.status_changed", "commerce_adjustment_vouchers",
            voucher.Id, new { voucher.VoucherNo, From = fromStatus, To = command.Status }, cancellationToken);
        await dbContext.SaveChangesAsync(cancellationToken);
        return voucher;
    }

    public async Task<TenantAdjustmentVoucherEventList> GetAdjustmentVoucherEventsAsync(
        CommerceAdminActor actor,
        Guid voucherId,
        CancellationToken cancellationToken = default)
    {
        await AssertAdminAsync(actor, cancellationToken);
        var exists = await dbContext.CommerceAdjustmentVouchers.AnyAsync(
            item => item.TenantId == actor.TenantId && item.Id == voucherId,
            cancellationToken);
        if (!exists) throw new CommerceException("Adjustment voucher was not found.", "adjustment_voucher_not_found");

        var events = await dbContext.CommerceAdjustmentVoucherEvents.AsNoTracking()
            .Where(item => item.TenantId == actor.TenantId && item.VoucherId == voucherId)
            .OrderBy(item => item.CreatedAt)
            .ToArrayAsync(cancellationToken);
        return new TenantAdjustmentVoucherEventList(events);
    }

    public async Task<TenantAdjustmentReport> GetAdjustmentReportAsync(
        CommerceAdminActor actor,
        CancellationToken cancellationToken = default)
    {
        await AssertAdminAsync(actor, cancellationToken);
        return new TenantAdjustmentReport(
            await dbContext.CommerceAdjustmentVouchers.CountAsync(
                item => item.TenantId == actor.TenantId && item.Status == CommerceAdjustmentVoucherStatus.Draft,
                cancellationToken),
            await dbContext.CommerceAdjustmentVouchers.CountAsync(
                item => item.TenantId == actor.TenantId && item.Status == CommerceAdjustmentVoucherStatus.PendingReview,
                cancellationToken),
            await dbContext.CommerceAdjustmentVouchers.CountAsync(
                item => item.TenantId == actor.TenantId && item.Status == CommerceAdjustmentVoucherStatus.Approved,
                cancellationToken),
            await dbContext.CommerceAdjustmentVouchers.CountAsync(
                item => item.TenantId == actor.TenantId && item.Status == CommerceAdjustmentVoucherStatus.Closed,
                cancellationToken),
            await dbContext.CommerceAdjustmentVouchers
                .Where(item =>
                    item.TenantId == actor.TenantId && item.Status == CommerceAdjustmentVoucherStatus.Approved &&
                    item.Direction == CommerceAdjustmentDirection.IncreaseRevenue)
                .SumAsync(item => item.AmountCents, cancellationToken),
            await dbContext.CommerceAdjustmentVouchers
                .Where(item =>
                    item.TenantId == actor.TenantId && item.Status == CommerceAdjustmentVoucherStatus.Approved &&
                    item.Direction == CommerceAdjustmentDirection.DecreaseRevenue)
                .SumAsync(item => item.AmountCents, cancellationToken));
    }

    public async Task<TenantReconciliationItemList> GetReconciliationItemsAsync(
        CommerceAdminActor actor,
        Guid batchId,
        CancellationToken cancellationToken = default)
    {
        await AssertAdminAsync(actor, cancellationToken);
        var batchExists = await dbContext.CommerceReconciliationBatches.AnyAsync(
            item => item.TenantId == actor.TenantId && item.Id == batchId,
            cancellationToken);
        if (!batchExists)
            throw new CommerceException("Reconciliation batch was not found.", "reconciliation_batch_not_found");

        var items = await dbContext.CommerceReconciliationItems.AsNoTracking()
            .Where(item => item.TenantId == actor.TenantId && item.BatchId == batchId)
            .OrderBy(item => item.RowNo)
            .Take(500)
            .ToArrayAsync(cancellationToken);
        return new TenantReconciliationItemList(items);
    }

    public async Task<TenantReconciliationIssueEventList> GetReconciliationIssueEventsAsync(
        CommerceAdminActor actor,
        Guid issueId,
        CancellationToken cancellationToken = default)
    {
        await AssertAdminAsync(actor, cancellationToken);
        var issueExists = await dbContext.CommerceReconciliationIssues.AnyAsync(
            item => item.TenantId == actor.TenantId && item.Id == issueId,
            cancellationToken);
        if (!issueExists)
            throw new CommerceException("Reconciliation issue was not found.", "reconciliation_issue_not_found");

        var events = await dbContext.CommerceReconciliationIssueEvents.AsNoTracking()
            .Where(item => item.TenantId == actor.TenantId && item.IssueId == issueId)
            .OrderBy(item => item.CreatedAt)
            .ToArrayAsync(cancellationToken);
        return new TenantReconciliationIssueEventList(events);
    }

    public async Task<TenantCommerceAnomalySummary> GetAnomalySummaryAsync(
        CommerceAdminActor actor,
        CancellationToken cancellationToken = default)
    {
        await AssertAdminAsync(actor, cancellationToken);
        var openRefunds = await dbContext.CommerceRefundRequests.CountAsync(
            item => item.TenantId == actor.TenantId && item.Status == CommerceRefundStatus.Requested,
            cancellationToken);
        var processingRefunds = await dbContext.CommerceRefundRequests.CountAsync(
            item => item.TenantId == actor.TenantId && item.Status == CommerceRefundStatus.Processing,
            cancellationToken);
        var openIssues = await dbContext.CommerceReconciliationIssues.CountAsync(
            item => item.TenantId == actor.TenantId && item.Status != ReconciliationIssueStatus.Resolved &&
                    item.Status != ReconciliationIssueStatus.Ignored,
            cancellationToken);
        var failedBatches = await dbContext.CommerceReconciliationBatches.CountAsync(
            item => item.TenantId == actor.TenantId && item.Status == ReconciliationBatchStatus.Failed,
            cancellationToken);
        var pendingPayments = await dbContext.Payments.CountAsync(
            item => item.TenantId == actor.TenantId && item.Status == PaymentStatus.Pending,
            cancellationToken);
        var mismatchCount = await dbContext.CommerceReconciliationItems.CountAsync(
            item => item.TenantId == actor.TenantId &&
                    (item.MatchStatus == ReconciliationMatchStatus.AmountMismatch ||
                     item.MatchStatus == ReconciliationMatchStatus.StatusMismatch),
            cancellationToken);

        return new TenantCommerceAnomalySummary(
            openRefunds,
            processingRefunds,
            openIssues,
            failedBatches,
            pendingPayments,
            mismatchCount);
    }

    public async Task<ReconciliationImportPreview> PreviewReconciliationImportAsync(
        CommerceAdminActor actor,
        PreviewReconciliationImportCommand command,
        CancellationToken cancellationToken = default)
    {
        await AssertAdminAsync(actor, cancellationToken);
        return BuildImportPreview(command.Provider, command.Rows);
    }

    public async Task<CommerceReconciliationBatch> ImportReconciliationAsync(
        CommerceAdminActor actor,
        ImportReconciliationCommand command,
        CancellationToken cancellationToken = default)
    {
        await AssertAdminAsync(actor, cancellationToken);
        var preview = BuildImportPreview(command.Provider, command.Rows);
        var batch = new CommerceReconciliationBatch
        {
            TenantId = actor.TenantId,
            CreatedBy = actor.UserId,
            Provider = NormalizeProvider(command.Provider),
            BillDate = command.BillDate,
            BillType = command.BillType,
            Source = ReconciliationSource.ManualUpload,
            SourceName = command.SourceName.Trim(),
            SourceHash = preview.SourceHash,
            Status = preview.InvalidCount == 0
                ? ReconciliationBatchStatus.Completed
                : ReconciliationBatchStatus.CompletedWithIssues,
            TotalCount = preview.TotalCount,
            MatchedCount = preview.TotalCount - preview.InvalidCount,
            MismatchCount = preview.InvalidCount,
            AmountCents = preview.AmountCents,
            RefundAmountCents = preview.RefundAmountCents,
            CompletedAt = DateTimeOffset.UtcNow,
            Metadata = JsonSerializer.SerializeToElement(new
            {
                preview.PaymentCount,
                preview.RefundCount,
                preview.InvalidCount
            })
        };
        dbContext.CommerceReconciliationBatches.Add(batch);
        var rowNo = 0;
        foreach (var row in EnumerateImportRows(command.Rows))
        {
            rowNo++;
            var item = CreateReconciliationItem(actor.TenantId, batch.Id, rowNo, NormalizeProvider(command.Provider),
                row);
            dbContext.CommerceReconciliationItems.Add(item);
            if (item.MatchStatus != ReconciliationMatchStatus.Matched)
                dbContext.CommerceReconciliationIssues.Add(new CommerceReconciliationIssue
                {
                    TenantId = actor.TenantId,
                    BatchId = batch.Id,
                    Provider = item.Provider,
                    TransactionType = item.TransactionType,
                    IssueNo = $"RC{DateTimeOffset.UtcNow:yyyyMMddHHmmss}{rowNo:0000}",
                    MatchStatus = item.MatchStatus switch
                    {
                        ReconciliationMatchStatus.MissingLocal => ReconciliationIssueMatchStatus.MissingLocal,
                        ReconciliationMatchStatus.MissingProvider => ReconciliationIssueMatchStatus.MissingProvider,
                        ReconciliationMatchStatus.Duplicate => ReconciliationIssueMatchStatus.Duplicate,
                        ReconciliationMatchStatus.StatusMismatch => ReconciliationIssueMatchStatus.StatusMismatch,
                        _ => ReconciliationIssueMatchStatus.AmountMismatch
                    },
                    Severity = item.Severity,
                    Status = ReconciliationIssueStatus.Open,
                    OrderNo = item.OrderNo,
                    RefundNo = item.RefundNo,
                    ProviderTradeNo = item.ProviderTradeNo,
                    ProviderRefundNo = item.ProviderRefundNo,
                    AmountCents = item.AmountCents,
                    RefundAmountCents = item.RefundAmountCents,
                    Summary = item.IssueCode,
                    CreatedBy = actor.UserId,
                    Metadata = item.Details
                });
        }

        await AddAuditAsync(actor, "commerce.reconciliation.imported", "commerce_reconciliation_batches", batch.Id,
            new { batch.Provider, batch.BillDate, batch.TotalCount }, cancellationToken);
        await dbContext.SaveChangesAsync(cancellationToken);
        return batch;
    }

    public async Task<BackgroundJobItem> RequestProviderBillJobAsync(
        CommerceAdminActor actor,
        RequestProviderBillJobCommand command,
        CancellationToken cancellationToken = default)
    {
        await AssertAdminAsync(actor, cancellationToken);
        var job = await backgroundJobQueue.EnqueueAsync(
            new CreateBackgroundJobCommand(
                actor.TenantId,
                "commerce_reconciliation",
                JsonSerializer.SerializeToElement(new
                {
                    provider = NormalizeProvider(command.Provider),
                    command.BillDate,
                    billType = command.BillType.ToString()
                }),
                command.RunAfter,
                5),
            cancellationToken);
        await AddAuditAsync(actor, "commerce.reconciliation.provider_bill_requested", "background_jobs", job.Id,
            new { command.Provider, command.BillDate, command.BillType }, cancellationToken);
        await dbContext.SaveChangesAsync(cancellationToken);
        return job;
    }

    public async Task<IReadOnlyCollection<BackgroundJobItem>> GetProviderBillJobsAsync(
        CommerceAdminActor actor,
        CommerceAdminQuery query,
        CancellationToken cancellationToken = default)
    {
        await AssertAdminAsync(actor, cancellationToken);
        return await backgroundJobOperations.ListAsync(actor.TenantId, "commerce_reconciliation",
            Math.Clamp(query.Limit ?? 50, 1, 200), cancellationToken);
    }

    public async Task<CommerceRefundRequest> ProcessRefundNotificationAsync(
        Guid tenantId,
        RefundNotificationCommand command,
        CancellationToken cancellationToken = default)
    {
        var provider = NormalizeProvider(command.Provider);
        var refund = await dbContext.CommerceRefundRequests.SingleOrDefaultAsync(
            item => item.TenantId == tenantId && item.RefundNo == command.RefundNo,
            cancellationToken) ?? throw new CommerceException("Refund request was not found.", "refund_not_found");
        var eventId = string.IsNullOrWhiteSpace(command.EventId)
            ? $"{provider}:{command.RefundNo}:{command.Status}"
            : command.EventId.Trim();
        var duplicate = await dbContext.PaymentEvents.AnyAsync(
            item => item.TenantId == tenantId &&
                    item.Provider == provider &&
                    item.EventType == "refund" &&
                    item.EventId == eventId,
            cancellationToken);
        if (duplicate) return refund;

        dbContext.PaymentEvents.Add(new PaymentEvent
        {
            TenantId = tenantId,
            Provider = provider,
            EventType = "refund",
            EventId = eventId,
            SignatureValid = true,
            Payload = JsonObjectOrDefault(command.Payload),
            ProcessedAt = DateTimeOffset.UtcNow
        });
        var fromStatus = refund.Status;
        if (fromStatus != command.Status && IsAllowedRefundTransition(fromStatus, command.Status))
        {
            refund.Status = command.Status;
            refund.ProviderRefundNo = string.IsNullOrWhiteSpace(command.ProviderRefundNo)
                ? refund.ProviderRefundNo
                : command.ProviderRefundNo.Trim();
            if (command.Status == CommerceRefundStatus.Succeeded)
            {
                refund.SucceededAt = DateTimeOffset.UtcNow;
                await ApplyRefundToOrderAsync(refund, cancellationToken);
            }
            else if (command.Status == CommerceRefundStatus.Failed)
            {
                refund.FailedAt = DateTimeOffset.UtcNow;
            }

            dbContext.CommerceRefundEvents.Add(new CommerceRefundEvent
            {
                TenantId = tenantId,
                RefundRequestId = refund.Id,
                FromStatus = fromStatus,
                ToStatus = command.Status,
                EventType = "provider_notify",
                Details = JsonObjectOrDefault(command.Payload)
            });
        }

        await dbContext.SaveChangesAsync(cancellationToken);
        return refund;
    }
}