using System.Collections.Generic;
using CMSMicroservice.Application.PackageCQ.Commands.VerifyUserPackagePurchasePayment;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.DiscountShopCQ.Commands.CompleteOrderPayment;
using CMSMicroservice.Application.WalletCQ.Commands.VerifyDiscountWalletCharge;
using CMSMicroservice.Application.WalletCQ.Commands.VerifyCreditWalletCharge;
using CMSMicroservice.Application.WalletCQ.Commands.VerifyMagicWalletCharge;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Entities.Payment;
using CMSMicroservice.Domain.Enums;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace CMSMicroservice.Infrastructure.BackgroundJobs;

/// <summary>
/// Hangfire job that reconciles Zarinpal payments whose return-URL callback was never received.
///
/// Runs every 30 minutes. For each payment older than 30 minutes that is still pending in our DB:
///   1. Cross-references with Zarinpal's unVerified list (payments received by bank but not yet
///      acknowledged by us). Falls back to a direct DB scan if ZP API returns nothing.
///   2. Determines payment type from the stored CallbackUrl.
///   3. Runs the same post-verification business logic that the callback endpoint would have run.
///
/// This ensures users whose browser crashed or whose network dropped after paying never lose
/// their wallet credits, package activations, or order completions.
/// </summary>
public class ZarinpalReconciliationJob
{
    private readonly IApplicationDbContext _context;
    private readonly IPaymentGatewayService _paymentGateway;
    private readonly ISender _sender;
    private readonly ILogger<ZarinpalReconciliationJob> _logger;

    public ZarinpalReconciliationJob(
        IApplicationDbContext context,
        IPaymentGatewayService paymentGateway,
        ISender sender,
        ILogger<ZarinpalReconciliationJob> logger)
    {
        _context = context;
        _paymentGateway = paymentGateway;
        _sender = sender;
        _logger = logger;
    }

    public async Task ExecuteAsync(CancellationToken ct = default)
    {
        _logger.LogInformation("ZarinpalReconciliationJob: started");

        try
        {
            var pendingTxs = await ResolvePendingTransactionsAsync(ct);

            if (pendingTxs.Count == 0)
            {
                _logger.LogInformation("ZarinpalReconciliationJob: no pending transactions to reconcile");
                return;
            }

            _logger.LogInformation(
                "ZarinpalReconciliationJob: {Count} pending transaction(s) to process", pendingTxs.Count);

            var processed = 0;
            var failed = 0;

            foreach (var tx in pendingTxs)
            {
                try
                {
                    await ProcessPaymentAsync(tx, ct);
                    processed++;
                    _logger.LogInformation(
                        "ZarinpalReconciliationJob: ✅ processed authority={Authority}", tx.Authority);
                }
                catch (Exception ex)
                {
                    failed++;
                    _logger.LogError(
                        ex,
                        "ZarinpalReconciliationJob: ❌ failed for authority={Authority}, type={Type}",
                        tx.Authority,
                        DeterminePaymentType(tx.CallbackUrl ?? string.Empty));
                }
            }

            _logger.LogInformation(
                "ZarinpalReconciliationJob: finished — processed={Processed}, failed={Failed}",
                processed, failed);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "ZarinpalReconciliationJob: unexpected top-level error");
            throw;
        }
    }

    // ── Candidate resolution ─────────────────────────────────────────────────────────────────

    private async Task<List<PaymentTransaction>> ResolvePendingTransactionsAsync(CancellationToken ct)
    {
        // Try Zarinpal's own "unVerified" list first (most accurate — only paid-but-not-verified)
        var zpAuthorities = await _paymentGateway.GetUnverifiedAuthoritiesAsync(ct);

        var cutoff = DateTime.UtcNow.AddMinutes(-30);   // older than 30 min
        var maxAge = DateTime.UtcNow.AddHours(-24);      // but within 24 h (ZP window)

        if (zpAuthorities.Count > 0)
        {
            _logger.LogInformation(
                "ZarinpalReconciliationJob: ZP unVerified list has {Count} authority(ies), cross-referencing with DB",
                zpAuthorities.Count);

            return await _context.PaymentTransactions
                .Where(pt =>
                    !pt.PaymentStatus &&
                    pt.RequestStatusCode == 100 &&
                    pt.Authority != null &&
                    zpAuthorities.Contains(pt.Authority) &&
                    pt.Created < cutoff &&
                    pt.Created > maxAge)
                .ToListAsync(ct);
        }else{
            _logger.LogInformation(
                "ZarinpalReconciliationJob: ZP unVerified list is empty");
        }
        // Fallback: query our own DB for stale pending transactions and try to verify them.
        // Zarinpal's verify call will return an error for any authority where payment was NOT made,
        // so this is safe — we only apply business logic when ZP confirms success (code 100/101).
        _logger.LogInformation(
            "ZarinpalReconciliationJob: ZP API returned empty list; falling back to DB scan (Created < {Cutoff})",
            cutoff);

        return await _context.PaymentTransactions
            .Where(pt =>
                !pt.PaymentStatus &&
                pt.RequestStatusCode == 100 &&
                pt.Authority != null &&
                pt.GatewayProvider == "zarinpal" &&
                pt.Created < cutoff &&
                pt.Created > maxAge)
            .ToListAsync(ct);
    }

    // ── Payment type dispatch ────────────────────────────────────────────────────────────────

    private async Task ProcessPaymentAsync(PaymentTransaction paymentTx, CancellationToken ct)
    {
        var authority = paymentTx.Authority!;
        var callbackUrl = paymentTx.CallbackUrl ?? string.Empty;
        var paymentType = DeterminePaymentType(callbackUrl);

        _logger.LogInformation(
            "ZarinpalReconciliation: processing type={Type}, authority={Authority}, userId={UserId}",
            paymentType, authority, paymentTx.UserId);

        switch (paymentType)
        {
            case "magic-wallet":
                await _sender.Send(
                    new VerifyMagicWalletChargeCommand { Authority = authority, Status = "OK" }, ct);
                break;

            case "discount-wallet":
                if (!paymentTx.UserId.HasValue)
                    throw new InvalidOperationException(
                        $"UserId is null for discount-wallet authority {authority}");

                await _sender.Send(new VerifyDiscountWalletChargeCommand
                {
                    Authority = authority,
                    UserId = paymentTx.UserId.Value,
                    Amount = paymentTx.Amount
                }, ct);
                break;

            case "credit-wallet":
                if (!paymentTx.UserId.HasValue)
                    throw new InvalidOperationException(
                        $"UserId is null for credit-wallet authority {authority}");

                await _sender.Send(new VerifyCreditWalletChargeCommand
                {
                    Authority = authority,
                    UserId = paymentTx.UserId.Value,
                    Amount = paymentTx.Amount
                }, ct);
                break;

            case "discount-order":
                await ReconcileDiscountOrderAsync(paymentTx, ct);
                break;

            case "package":
                await ReconcilePackagePurchaseAsync(paymentTx, ct);
                break;

            default:
                await ReconcileGenericTransactionAsync(paymentTx, ct);
                break;
        }
    }

    private static string DeterminePaymentType(string callbackUrl)
    {
        if (callbackUrl.Contains("type=magic-wallet", StringComparison.OrdinalIgnoreCase))
            return "magic-wallet";
        if (callbackUrl.Contains("type=discount-wallet", StringComparison.OrdinalIgnoreCase))
            return "discount-wallet";
        if (callbackUrl.Contains("type=credit-wallet", StringComparison.OrdinalIgnoreCase))
            return "credit-wallet";
        if (callbackUrl.Contains("type=discount-order", StringComparison.OrdinalIgnoreCase))
            return "discount-order";
        // Package callbacks have orderId= but no type= parameter
        if (callbackUrl.Contains("orderId=", StringComparison.OrdinalIgnoreCase) &&
            !callbackUrl.Contains("type=", StringComparison.OrdinalIgnoreCase))
            return "package";
        return "generic";
    }

    // ── Per-type reconcile helpers ───────────────────────────────────────────────────────────

    private async Task ReconcileDiscountOrderAsync(PaymentTransaction paymentTx, CancellationToken ct)
    {
        if (!long.TryParse(paymentTx.OrderId, out var orderId))
            throw new InvalidOperationException(
                $"Cannot parse OrderId '{paymentTx.OrderId}' from PaymentTransaction {paymentTx.Id}");

        var order = await _context.DiscountOrders
            .Include(o => o.OrderDetails)
            .FirstOrDefaultAsync(o => o.Id == orderId, ct)
            ?? throw new InvalidOperationException($"DiscountOrder #{orderId} not found");

        var transaction = order.TransactionId.HasValue
            ? await _context.Transactions.FirstOrDefaultAsync(t => t.Id == order.TransactionId.Value, ct)
            : null;

        var verifyResult = await _paymentGateway.VerifyPaymentAsync(
            paymentTx.Authority!, "OK", order.GatewayAmountPaid, ct);

        paymentTx.PaymentStatus = verifyResult.IsSuccess;
        paymentTx.VerificationStatusCode = verifyResult.VerificationCode;
        paymentTx.VerificationStatusMessage = verifyResult.Message;
        paymentTx.CardPan = verifyResult.CardPan;
        paymentTx.CardHash = verifyResult.CardHash;
        paymentTx.RefId = verifyResult.TrackingCode;
        await _context.SaveChangesAsync(ct);

        var completeResult = await _sender.Send(new CompleteOrderPaymentCommand
        {
            OrderId = orderId,
            TransactionId = transaction?.Id ?? 0,
            PaymentSuccess = verifyResult.IsSuccess,
            RefId = verifyResult.TrackingCode ?? verifyResult.RefId
        }, ct);

        if (!completeResult.Success)
            _logger.LogWarning(
                "ZarinpalReconciliation: CompleteOrderPayment returned failure for order {OrderId}: {Msg}",
                orderId, completeResult.Message);
    }

    private async Task ReconcilePackagePurchaseAsync(PaymentTransaction paymentTx, CancellationToken ct)
    {
        if (!long.TryParse(paymentTx.OrderId, out var purchaseId))
            throw new InvalidOperationException(
                $"Cannot parse OrderId '{paymentTx.OrderId}' for package purchase {paymentTx.Id}");

        var result = await _sender.Send(new VerifyUserPackagePurchasePaymentCommand
        {
            PurchaseId = purchaseId,
            Authority = paymentTx.Authority ?? string.Empty,
            Status = "OK"
        }, ct);

        if (!result.Success)
        {
            throw new InvalidOperationException(
                $"Package verify failed for purchase #{purchaseId}, authority={paymentTx.Authority}: {result.Message}");
        }

        if (result.AlreadyPaid)
        {
            _logger.LogInformation(
                "ZarinpalReconciliation: package purchase #{PurchaseId} already verified (authority={Authority})",
                purchaseId,
                paymentTx.Authority);
        }
    }

    private async Task ReconcileGenericTransactionAsync(PaymentTransaction paymentTx, CancellationToken ct)
    {
        // Generic deposits: Transaction.RefId holds the Authority before verify
        var transaction = await _context.Transactions
            .Where(t => t.RefId == paymentTx.Authority && !t.IsDeleted)
            .FirstOrDefaultAsync(ct);

        if (transaction == null)
        {
            _logger.LogWarning(
                "ZarinpalReconciliation: no Transaction found with RefId={Authority} — skipping",
                paymentTx.Authority);
            return;
        }

        var amountInToman = paymentTx.Amount != 0 ? paymentTx.Amount : transaction.Amount;

        var verifyResult = await _paymentGateway.VerifyPaymentAsync(
            paymentTx.Authority!, "OK", (decimal)amountInToman, ct);

        paymentTx.PaymentStatus = verifyResult.IsSuccess;
        paymentTx.VerificationStatusCode = verifyResult.VerificationCode;
        paymentTx.VerificationStatusMessage = verifyResult.Message;
        paymentTx.CardPan = verifyResult.CardPan;
        paymentTx.CardHash = verifyResult.CardHash;
        paymentTx.RefId = verifyResult.TrackingCode;

        if (verifyResult.IsSuccess)
        {
            transaction.PaymentStatus = PaymentStatus.Success;
            transaction.PaymentDate = DateTime.UtcNow;
            transaction.RefId = verifyResult.RefId;
        }
        else
        {
            transaction.PaymentStatus = PaymentStatus.Reject;
        }

        await _context.SaveChangesAsync(ct);
    }
}
