package com.ruoyi.app.pay;

import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.JSONArray;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.ruoyi.app.pay.dto.LinePayCreateResult;
import com.ruoyi.app.pay.dto.LinePayQueryResult;
import com.ruoyi.app.utils.linepay.LinePayClient;
import com.ruoyi.app.utils.linepay.LinePayCredential;
import com.ruoyi.app.utils.linepay.LinePayRequest;
import com.ruoyi.app.utils.linepay.LinePayResponse;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.system.domain.PosOrder;
import com.ruoyi.system.domain.PosOrderLinePayment;
import com.ruoyi.system.domain.PosOrderLineRefund;
import com.ruoyi.system.domain.PosStoreLinePay;
import com.ruoyi.system.service.IPosOrderLinePaymentService;
import com.ruoyi.system.service.IPosOrderLineRefundService;
import com.ruoyi.system.service.IPosOrderService;
import com.ruoyi.system.service.IPosStoreLinePayService;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Value;

import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;

/** LINE Pay create, query, confirm, retrieval and payment-fact orchestration. */
@Service
public class LinePayService {

    public static final String PAY_TYPE_LINE = "3";
    private static final List<String> DEFINITE_REQUEST_FAILURE_CODES = List.of(
            "1104", "1105", "1106", "1124", "1178", "1183", "1194", "2101", "2102");
    private static final Set<String> CREDENTIAL_AUTH_FAILURE_CODES = Set.of("1104", "1105", "1106");
    @Value("${line-pay.reconcile.auth-deadline-minutes:30}")
    private long authDeadlineMinutes = 30L;
    @Value("${line-pay.reconcile.unknown-deadline-hours:24}")
    private long unknownDeadlineHours = 24L;
    @Value("${line-pay.reconcile.row-lease-seconds:120}")
    private long rowLeaseSeconds = 120L;

    private final IPosOrderService orderService;
    private final IPosStoreLinePayService credentialService;
    private final IPosOrderLinePaymentService paymentService;
    private final IPosOrderLineRefundService refundService;
    private final LinePayClient linePayClient;
    private final LinePayFactService factService;
    private final LinePayGatewayAuditService auditService;
    private final PaymentCreateGuardService createGuard;
    private final LinePayOfflineService offlineService;

    public LinePayService(IPosOrderService orderService,
                          IPosStoreLinePayService credentialService,
                          IPosOrderLinePaymentService paymentService,
                          IPosOrderLineRefundService refundService,
                          LinePayClient linePayClient,
                          LinePayFactService factService) {
        this(orderService, credentialService, paymentService, refundService,
                linePayClient, factService, null, null, null);
    }

    @Autowired
    public LinePayService(IPosOrderService orderService,
                          IPosStoreLinePayService credentialService,
                          IPosOrderLinePaymentService paymentService,
                          IPosOrderLineRefundService refundService,
                          LinePayClient linePayClient,
                          LinePayFactService factService,
                          LinePayGatewayAuditService auditService,
                          PaymentCreateGuardService createGuard,
                          LinePayOfflineService offlineService) {
        this.orderService = orderService;
        this.credentialService = credentialService;
        this.paymentService = paymentService;
        this.refundService = refundService;
        this.linePayClient = linePayClient;
        this.factService = factService;
        this.auditService = auditService;
        this.createGuard = createGuard;
        this.offlineService = offlineService;
    }

    public LinePayCreateResult create(Long userId, String ddId) {
        if (createGuard != null) {
            return createGuard.withLock(ddId, () -> createUnderLock(userId, ddId));
        }
        return createUnderLock(userId, ddId);
    }

    private LinePayCreateResult createUnderLock(Long userId, String ddId) {
        PosOrder order = requirePayableOrder(userId, ddId);
        requireSingleStoreOrder(order);
        PosStoreLinePay credential = credentialService.getEnabledCurrent(order.getMdId());
        if (credential == null) {
            throw new ServiceException("LINE Pay is not enabled for this store");
        }
        PosOrderLinePayment active = paymentService.getActiveByDdId(ddId);
        if (active != null) {
            return createResult(active, true);
        }

        IntentSelection selection = createIntent(order, credential);
        PosOrderLinePayment intent = selection.payment();
        if (!selection.created()) {
            return createResult(intent, true);
        }
        try {
            LinePayResponse response = gatewayCall("REQUEST", "APP", intent,
                    () -> linePayClient.request(toCredential(credential),
                            new LinePayRequest(intent.getLineOrderId(), ddId,
                                    "Order " + ddId, order.getAmount(), "TWD")));
            if (!response.isSuccess() || response.transactionId() == null) {
                if (isDefiniteRequestFailure(response)) {
                    if (paymentService.markTerminal(intent.getId(), intent.getVersion(),
                            "REQUESTING", "FAILED") == 1) {
                        intent.setStatus("FAILED");
                        intent.setActiveDdId(null);
                        return createResult(intent, false);
                    }
                    PosOrderLinePayment latest = paymentService.getById(intent.getId());
                    if (latest != null) {
                        return createResult(latest, true);
                    }
                    throw new ServiceException("LINE Pay payment state requires reconciliation");
                }
                paymentService.markRequestUnknown(intent.getId(), intent.getVersion(),
                        shortly(), unknownDeadline());
                intent.setStatus("REQUEST_UNKNOWN");
                return createResult(intent, false);
            }
            JSONObject paymentUrl = response.info() == null
                    ? null : response.info().getJSONObject("paymentUrl");
            String web = paymentUrl == null ? null : paymentUrl.getString("web");
            String app = paymentUrl == null ? null : paymentUrl.getString("app");
            if (web == null || paymentService.markRequestSucceeded(intent.getId(), intent.getVersion(),
                    response.transactionId(), web, app, shortly(), authDeadline()) != 1) {
                paymentService.markRequestUnknown(intent.getId(), intent.getVersion(),
                        shortly(), unknownDeadline());
                intent.setStatus("REQUEST_UNKNOWN");
                return createResult(intent, false);
            }
            intent.setTransactionId(response.transactionId());
            intent.setPaymentUrlWeb(web);
            intent.setPaymentUrlApp(app);
            intent.setStatus("WAITING_AUTH");
            return createResult(intent, false);
        } catch (Exception unknown) {
            paymentService.markRequestUnknown(intent.getId(), intent.getVersion(),
                    shortly(), unknownDeadline());
            intent.setStatus("REQUEST_UNKNOWN");
            return createResult(intent, false);
        }
    }

    public LinePayQueryResult query(Long userId, String ddId) {
        PosOrder order = requireOwnedOrder(userId, ddId);
        List<PosOrderLinePayment> attempts = paymentService.getByDdId(ddId);
        PosOrderLinePayment selected = selectForApp(order, attempts);
        LinePayQueryResult result = new LinePayQueryResult();
        result.setDdId(ddId);
        result.setOrderPayStatus(order.getPayStatus());
        if (selected != null) {
            result.setPaymentId(selected.getId());
            result.setPaymentStatus(selected.getStatus());
            result.setTransactionId(selected.getTransactionId());
            result.setUpdatedAt(selected.getUpdateTime());
            PosOrderLineRefund refund = refundService.getByPaymentId(selected.getId());
            result.setRefundStatus(refund == null ? null : refund.getStatus());
        }
        return result;
    }

    public PosOrderLinePayment findByLineOrderId(String lineOrderId, String transactionId) {
        PosOrderLinePayment payment = paymentService.getByLineOrderId(lineOrderId);
        if (payment == null) {
            return null;
        }
        if (transactionId != null && payment.getTransactionId() != null
                && !transactionId.equals(payment.getTransactionId())) {
            return null;
        }
        return payment;
    }

    public String reconcilePayment(Long paymentId, String source) {
        return reconcilePayment(paymentId, source, null);
    }

    public String reconcilePayment(Long paymentId, String source, String reconcileLeaseOwner) {
        PosOrderLinePayment payment = paymentService.getById(paymentId);
        if (payment == null) {
            throw new ServiceException("LINE Pay payment not found");
        }
        if ("OFFLINE".equals(payment.getPaymentMode()) && offlineService != null) {
            return offlineService.reconcilePayment(paymentId, source);
        }
        if (payment.getReconcileDeadline() != null
                && !new Date().before(payment.getReconcileDeadline())) {
            return finalDeadlineReconcile(payment, source);
        }
        PosStoreLinePay credential = credentialService.getById(payment.getCredentialId());
        if (credential == null) {
            throw new ServiceException("LINE Pay credential version not found");
        }
        try {
            RetrieveResult retrieveResult = retrieveWithCredentialRecovery(
                    payment, source, credential, toCredential(credential));
            LinePayResponse retrieve = retrieveResult.response();
            LinePayCredential gatewayCredential = retrieveResult.credential();
            JSONObject captured = capturedPayment(retrieve, payment);
            if (captured != null) {
                String fullRefundTransactionId = fullRefundTransactionId(captured, payment.getAmount());
                if (fullRefundTransactionId != null) {
                    factService.applyPaidAndRefundedFact(payment.getId(),
                            captured.getString("transactionId"), paymentProvider(retrieve), new Date(),
                            fullRefundTransactionId, new Date());
                    return "REFUNDED";
                }
                if (hasRefundEvidence(captured)) {
                    if (isLocallyRefunded(payment.getId())) {
                        return "REFUNDED";
                    }
                    factService.applyPaidWithRefundReviewFact(payment.getId(),
                            captured.getString("transactionId"), paymentProvider(retrieve), new Date());
                    return "MANUAL_REVIEW";
                }
                factService.applyPaidFact(payment.getId(), captured.getString("transactionId"),
                        paymentProvider(retrieve), new Date());
                return "PAID";
            }
            if (!("1150".equals(retrieve.returnCode()) || retrieve.isSuccess())) {
                return payment.getStatus();
            }
            if (!"1150".equals(retrieve.returnCode())
                    || payment.getTransactionId() == null
                    || !canCheckOrConfirm(payment.getStatus())) {
                return payment.getStatus();
            }
            LinePayResponse check = gatewayCall("CHECK", source, payment,
                    () -> linePayClient.check(gatewayCredential, payment.getTransactionId()));
            return handleCheck(payment, gatewayCredential, check, source, reconcileLeaseOwner);
        } catch (Exception unknown) {
            if ("CONFIRMING".equals(payment.getStatus())) {
                paymentService.markConfirmUnknown(payment.getId(), payment.getVersion(),
                        shortly(), unknownDeadline());
                return "CONFIRM_UNKNOWN";
            }
            return payment.getStatus();
        }
    }

    private String finalDeadlineReconcile(PosOrderLinePayment payment, String source) {
        PosStoreLinePay credential = credentialService.getById(payment.getCredentialId());
        if (credential == null) {
            paymentService.markManualReview(payment.getId(), payment.getVersion(), payment.getStatus());
            return "MANUAL_REVIEW";
        }
        try {
            RetrieveResult retrieveResult = retrieveWithCredentialRecovery(
                    payment, source, credential, toCredential(credential));
            LinePayCredential gatewayCredential = retrieveResult.credential();
            LinePayResponse retrieve = retrieveResult.response();
            JSONObject captured = capturedPayment(retrieve, payment);
            if (captured != null) {
                String fullRefundTransactionId = fullRefundTransactionId(captured, payment.getAmount());
                if (fullRefundTransactionId != null) {
                    factService.applyPaidAndRefundedFact(payment.getId(),
                            captured.getString("transactionId"), paymentProvider(retrieve), new Date(),
                            fullRefundTransactionId, new Date());
                    return "REFUNDED";
                }
                if (hasRefundEvidence(captured)) {
                    if (isLocallyRefunded(payment.getId())) {
                        return "REFUNDED";
                    }
                    factService.applyPaidWithRefundReviewFact(payment.getId(),
                            captured.getString("transactionId"), paymentProvider(retrieve), new Date());
                    return "MANUAL_REVIEW";
                }
                factService.applyPaidFact(payment.getId(), captured.getString("transactionId"),
                        paymentProvider(retrieve), new Date());
                return "PAID";
            }
            if ("1150".equals(retrieve.returnCode()) && payment.getTransactionId() != null
                    && canCheckOrConfirm(payment.getStatus())) {
                LinePayResponse check = gatewayCall("CHECK", source, payment,
                        () -> linePayClient.check(gatewayCredential, payment.getTransactionId()));
                if ("0121".equals(check.returnCode()) || "0122".equals(check.returnCode())) {
                    String terminal = "0121".equals(check.returnCode())
                            ? "CANCELLED_OR_EXPIRED" : "FAILED";
                    paymentService.markTerminal(payment.getId(), payment.getVersion(),
                            payment.getStatus(), terminal);
                    return terminal;
                }
            }
        } catch (Exception ignored) {
            // The final read was inconclusive; stop automatic scanning for manual review.
        }
        paymentService.markManualReview(payment.getId(), payment.getVersion(), payment.getStatus());
        return "MANUAL_REVIEW";
    }

    private String handleCheck(PosOrderLinePayment payment, LinePayCredential credential,
                               LinePayResponse check, String source,
                               String reconcileLeaseOwner) throws Exception {
        String code = check.returnCode();
        if ("0000".equals(code)) {
            return payment.getStatus();
        }
        if ("0121".equals(code) || "0122".equals(code)) {
            String terminal = "0121".equals(code) ? "CANCELLED_OR_EXPIRED" : "FAILED";
            paymentService.markTerminal(payment.getId(), payment.getVersion(), payment.getStatus(), terminal);
            return terminal;
        }
        if ("0123".equals(code)) {
            LinePayResponse retrieve = gatewayCall("RETRIEVE", source, payment,
                    () -> payment.getTransactionId() == null
                            ? linePayClient.retrieveByOrderId(credential, payment.getLineOrderId())
                            : linePayClient.retrieveByTransactionId(credential, payment.getTransactionId()));
            JSONObject captured = capturedPayment(retrieve, payment);
            if (captured != null) {
                String fullRefundTransactionId = fullRefundTransactionId(captured, payment.getAmount());
                if (fullRefundTransactionId != null) {
                    factService.applyPaidAndRefundedFact(payment.getId(),
                            captured.getString("transactionId"), paymentProvider(retrieve), new Date(),
                            fullRefundTransactionId, new Date());
                    return "REFUNDED";
                }
                if (hasRefundEvidence(captured)) {
                    if (isLocallyRefunded(payment.getId())) {
                        return "REFUNDED";
                    }
                    factService.applyPaidWithRefundReviewFact(payment.getId(),
                            captured.getString("transactionId"), paymentProvider(retrieve), new Date());
                    return "MANUAL_REVIEW";
                }
                factService.applyPaidFact(payment.getId(), captured.getString("transactionId"),
                        paymentProvider(retrieve), new Date());
                return "PAID";
            }
            return payment.getStatus();
        }
        if (!"0110".equals(code)) {
            return payment.getStatus();
        }
        if (!canCheckOrConfirm(payment.getStatus())) {
            return payment.getStatus();
        }
        PosOrder order = orderService.getOne(new QueryWrapper<PosOrder>().eq("dd_id", payment.getDdId()));
        if (order == null || !isPayableOrderState(order)) {
            paymentService.markAuthDoneOrderCancelled(payment.getId(), payment.getVersion(),
                    payment.getStatus(), shortly(), authDeadline());
            return "AUTH_DONE_ORDER_CANCELLED";
        }
        if (!LinePayService.PAY_TYPE_LINE.equals(order.getPayType())
                || Long.valueOf(1L).equals(order.getPayStatus())
                || !order.getMdId().equals(payment.getStoreId())
                || !order.getAmount().equals(payment.getAmount())) {
            return payment.getStatus();
        }
        String leaseOwner = reconcileLeaseOwner == null
                ? (source == null ? "LINE" : source) + "-" + UUID.randomUUID()
                : reconcileLeaseOwner;
        if (paymentService.claimConfirm(payment.getId(), payment.getVersion(), payment.getStatus(),
                leaseOwner, leaseUntil()) != 1) {
            return payment.getStatus();
        }
        LinePayResponse confirm;
        try {
            confirm = gatewayCall("CONFIRM", source, payment,
                    () -> linePayClient.confirm(credential, payment.getTransactionId(),
                            payment.getAmount(), payment.getCurrency()));
        } catch (Exception unknown) {
            paymentService.markConfirmUnknown(payment.getId(), payment.getVersion() + 1,
                    shortly(), unknownDeadline());
            return "CONFIRM_UNKNOWN";
        }
        if (confirm.isSuccess()) {
            if (!confirmedPayment(confirm, payment)) {
                paymentService.markConfirmUnknown(payment.getId(), payment.getVersion() + 1,
                        shortly(), unknownDeadline());
                return "CONFIRM_UNKNOWN";
            }
            factService.applyPaidFact(payment.getId(), payment.getTransactionId(),
                    paymentProvider(confirm), new Date());
            return "PAID";
        }
        paymentService.markConfirmUnknown(payment.getId(), payment.getVersion() + 1,
                shortly(), unknownDeadline());
        return "CONFIRM_UNKNOWN";
    }

    private static JSONObject capturedPayment(LinePayResponse response, PosOrderLinePayment payment) {
        if (response == null || !response.isSuccess()) {
            return null;
        }
        JSONObject root = JSONObject.parseObject(response.rawBody());
        JSONArray info = root.getJSONArray("info");
        if (info == null || info.size() != 1) {
            return null;
        }
        JSONObject transaction = info.getJSONObject(0);
        String gatewayTransactionId = transaction.getString("transactionId");
        boolean transactionMatches = payment.getTransactionId() == null
                ? gatewayTransactionId != null
                : payment.getTransactionId().equals(gatewayTransactionId);
        Integer gatewayAmount = paymentAmount(transaction);
        return transactionMatches
                && payment.getLineOrderId().equals(transaction.getString("orderId"))
                && payment.getCurrency().equals(transaction.getString("currency"))
                && "PAYMENT".equals(transaction.getString("transactionType"))
                && gatewayAmount != null && gatewayAmount.equals(payment.getAmount())
                ? transaction : null;
    }

    private static Integer paymentAmount(JSONObject transaction) {
        JSONArray payInfo = transaction.getJSONArray("payInfo");
        if (payInfo == null || payInfo.isEmpty()) {
            return null;
        }
        long total = 0L;
        for (int i = 0; i < payInfo.size(); i++) {
            JSONObject item = payInfo.getJSONObject(i);
            Integer amount = item == null ? null : item.getInteger("amount");
            if (amount == null || amount <= 0) {
                return null;
            }
            total += amount;
        }
        return total <= Integer.MAX_VALUE ? (int) total : null;
    }

    private static String fullRefundTransactionId(JSONObject transaction, int expectedAmount) {
        JSONArray refunds = transaction.getJSONArray("refundList");
        if (refunds == null || refunds.isEmpty()) {
            return null;
        }
        long total = 0L;
        String lastTransactionId = null;
        for (int i = 0; i < refunds.size(); i++) {
            JSONObject refund = refunds.getJSONObject(i);
            Integer amount = refund == null ? null : refund.getInteger("refundAmount");
            String transactionId = refund == null ? null : refund.getString("refundTransactionId");
            if (amount == null || amount == 0 || transactionId == null) {
                return null;
            }
            total += Math.abs((long) amount);
            lastTransactionId = transactionId;
        }
        return total == expectedAmount ? lastTransactionId : null;
    }

    private static boolean hasRefundEvidence(JSONObject transaction) {
        JSONArray refunds = transaction.getJSONArray("refundList");
        return refunds != null && !refunds.isEmpty();
    }

    private static boolean canCheckOrConfirm(String status) {
        return "WAITING_AUTH".equals(status) || "READY_CONFIRM".equals(status);
    }

    private static String paymentProvider(LinePayResponse response) {
        if (response == null || response.rawBody() == null) {
            return null;
        }
        JSONObject root = JSONObject.parseObject(response.rawBody());
        JSONObject objectInfo = root.getJSONObject("info");
        if (objectInfo != null) {
            return objectInfo.getString("paymentProvider");
        }
        JSONArray info = root.getJSONArray("info");
        return info != null && info.size() == 1
                ? info.getJSONObject(0).getString("paymentProvider") : null;
    }

    private static boolean confirmedPayment(LinePayResponse response, PosOrderLinePayment payment) {
        if (response == null || !response.isSuccess() || response.rawBody() == null) {
            return false;
        }
        JSONObject info = JSONObject.parseObject(response.rawBody()).getJSONObject("info");
        if (info == null || !payment.getTransactionId().equals(info.getString("transactionId"))) {
            return false;
        }
        String orderId = info.getString("orderId");
        String currency = info.getString("currency");
        return (orderId == null || payment.getLineOrderId().equals(orderId))
                && (currency == null || payment.getCurrency().equals(currency))
                && payment.getAmount().equals(paymentAmount(info));
    }

    private IntentSelection createIntent(PosOrder order, PosStoreLinePay credential) {
        for (int attempt = 0; attempt < 3; attempt++) {
            try {
                return new IntentSelection(paymentService.createRequesting(
                        String.valueOf(order.getDdId()), newLineOrderId(), credential.getId(),
                        order.getMdId(), order.getAmount(), "TWD", "ONLINE", unknownDeadline()), true);
            } catch (DuplicateKeyException duplicate) {
                PosOrderLinePayment concurrent = paymentService.getActiveByDdId(String.valueOf(order.getDdId()));
                if (concurrent != null) {
                    return new IntentSelection(concurrent, false);
                }
                if (attempt == 2) {
                    throw duplicate;
                }
            }
        }
        throw new IllegalStateException("Unable to create LINE Pay attempt");
    }

    private record IntentSelection(PosOrderLinePayment payment, boolean created) {
    }

    private PosOrder requirePayableOrder(Long userId, String ddId) {
        PosOrder order = requireOwnedOrder(userId, ddId);
        if (!PAY_TYPE_LINE.equals(order.getPayType()) || Long.valueOf(1L).equals(order.getPayStatus())
                || !isPayableOrderState(order) || order.getAmount() == null
                || order.getAmount() <= 0 || order.getMdId() == null) {
            throw new ServiceException("Order is not payable by LINE Pay");
        }
        return order;
    }

    private static boolean isPayableOrderState(PosOrder order) {
        return order.getState() != null && (order.getState() == 0L || order.getState() == 1L)
                && (order.getAfterSaleStatus() == null || order.getAfterSaleStatus() == 0L);
    }

    private static boolean isDefiniteRequestFailure(LinePayResponse response) {
        if (response == null || response.httpStatus() < 200 || response.httpStatus() >= 300
                || response.returnCode() == null || response.isSuccess()) {
            return false;
        }
        return DEFINITE_REQUEST_FAILURE_CODES.contains(response.returnCode());
    }

    private Date leaseUntil() {
        return new Date(System.currentTimeMillis() + Math.max(60L, rowLeaseSeconds) * 1000L);
    }

    private PosOrder requireOwnedOrder(Long userId, String ddId) {
        if (userId == null || ddId == null || ddId.trim().isEmpty()) {
            throw new ServiceException("Invalid order");
        }
        PosOrder order = orderService.getOne(new QueryWrapper<PosOrder>().eq("dd_id", ddId));
        if (order == null || order.getUserId() == null || !order.getUserId().equals(userId)) {
            throw new ServiceException("Order not found or forbidden");
        }
        return order;
    }

    private void requireSingleStoreOrder(PosOrder order) {
        String parentDdId = order.getParentDdId() == null
                ? String.valueOf(order.getDdId()) : order.getParentDdId();
        long count = orderService.count(new QueryWrapper<PosOrder>().eq("parent_dd_id", parentDdId));
        if (count > 1) {
            throw new ServiceException("LINE Pay supports single-store orders only");
        }
    }

    private PosOrderLinePayment selectForApp(PosOrder order, List<PosOrderLinePayment> attempts) {
        if (attempts == null || attempts.isEmpty()) {
            return null;
        }
        List<PosOrderLinePayment> paid = attempts.stream()
                .filter(payment -> "PAID".equals(payment.getStatus())).toList();
        List<PosOrderLinePayment> unrefunded = paid.stream().filter(payment -> {
            PosOrderLineRefund refund = refundService.getByPaymentId(payment.getId());
            return refund == null || !"REFUNDED".equals(refund.getStatus());
        }).toList();
        if (Long.valueOf(1L).equals(order.getPayStatus()) && unrefunded.size() == 1) {
            return unrefunded.get(0);
        }
        if (Long.valueOf(2L).equals(order.getPayStatus()) && !paid.isEmpty()) {
            return paid.stream().filter(payment -> {
                PosOrderLineRefund refund = refundService.getByPaymentId(payment.getId());
                return refund != null && "REFUNDED".equals(refund.getStatus());
            }).findFirst().orElse(paid.get(0));
        }
        if (unrefunded.size() > 1) {
            PosOrderLinePayment manual = unrefunded.get(0);
            manual.setStatus("MANUAL_REVIEW");
            return manual;
        }
        if (paid.size() == 1) {
            return paid.get(0);
        }
        return attempts.stream().filter(payment -> payment.getActiveDdId() != null).findFirst()
                .orElseGet(() -> attempts.stream().max(Comparator
                        .comparing(PosOrderLinePayment::getCreateTime,
                                Comparator.nullsFirst(Date::compareTo))
                        .thenComparing(PosOrderLinePayment::getId,
                                Comparator.nullsFirst(Long::compareTo))).orElse(null));
    }

    private static LinePayCredential toCredential(PosStoreLinePay credential) {
        return new LinePayCredential(credential.getId(), credential.getChannelId(),
                credential.getChannelSecret());
    }

    private RetrieveResult retrieveWithCredentialRecovery(PosOrderLinePayment payment, String source,
                                                           PosStoreLinePay originalVersion,
                                                           LinePayCredential originalCredential)
            throws Exception {
        LinePayResponse original = retrieve(payment, source, originalCredential);
        if (!isCredentialAuthFailure(original)) {
            return new RetrieveResult(original, originalCredential);
        }
        PosStoreLinePay current = credentialService.getCurrent(payment.getStoreId());
        if (current == null || Objects.equals(current.getId(), originalVersion.getId())
                || !Objects.equals(current.getChannelId(), originalVersion.getChannelId())
                || !Objects.equals(current.getEnvironment(), originalVersion.getEnvironment())) {
            return new RetrieveResult(original, originalCredential);
        }
        LinePayCredential currentCredential = toCredential(current);
        LinePayResponse proof = retrieve(payment, source, currentCredential);
        return capturedPayment(proof, payment) == null
                ? new RetrieveResult(original, originalCredential)
                : new RetrieveResult(proof, currentCredential);
    }

    private LinePayResponse retrieve(PosOrderLinePayment payment, String source,
                                     LinePayCredential credential) throws Exception {
        return gatewayCall("RETRIEVE", source, payment, credential.id(),
                () -> payment.getTransactionId() == null
                        ? linePayClient.retrieveByOrderId(credential, payment.getLineOrderId())
                        : linePayClient.retrieveByTransactionId(credential, payment.getTransactionId()));
    }

    private static boolean isCredentialAuthFailure(LinePayResponse response) {
        return response != null && CREDENTIAL_AUTH_FAILURE_CODES.contains(response.returnCode());
    }

    private boolean isLocallyRefunded(Long paymentId) {
        PosOrderLineRefund refund = refundService.getByPaymentId(paymentId);
        return refund != null && "REFUNDED".equals(refund.getStatus());
    }

    private record RetrieveResult(LinePayResponse response, LinePayCredential credential) {
    }

    private static LinePayCreateResult createResult(PosOrderLinePayment payment, boolean reused) {
        LinePayCreateResult result = new LinePayCreateResult();
        result.setDdId(payment.getDdId());
        result.setPaymentId(payment.getId());
        result.setLineOrderId(payment.getLineOrderId());
        result.setTransactionId(payment.getTransactionId());
        result.setPaymentUrl(payment.getPaymentUrlWeb());
        result.setStatus(payment.getStatus());
        result.setReusedAttempt(reused);
        return result;
    }

    private static String newLineOrderId() {
        return "LP" + UUID.randomUUID().toString().replace("-", "").toUpperCase(Locale.ROOT);
    }

    private <T> T gatewayCall(String action, String source, PosOrderLinePayment payment,
                              LinePayGatewayAuditService.GatewayCall<T> call) throws Exception {
        return gatewayCall(action, source, payment, payment.getCredentialId(), call);
    }

    private <T> T gatewayCall(String action, String source, PosOrderLinePayment payment,
                              Long credentialId,
                              LinePayGatewayAuditService.GatewayCall<T> call) throws Exception {
        if (auditService == null) {
            return call.call();
        }
        return auditService.execute(action, source == null ? "LINE" : source,
                payment.getId(), null, credentialId, payment.getStoreId(),
                payment.getDdId(), payment.getLineOrderId(), payment.getTransactionId(), call);
    }

    private static Date shortly() {
        return new Date(System.currentTimeMillis() + 30_000L);
    }

    private Date authDeadline() {
        return new Date(System.currentTimeMillis() + authDeadlineMinutes * 60L * 1000L);
    }

    private Date unknownDeadline() {
        return new Date(System.currentTimeMillis() + unknownDeadlineHours * 60L * 60L * 1000L);
    }
}
