package com.ruoyi.app.pay;

import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.ruoyi.app.pay.dto.LinePayOfflineResult;
import com.ruoyi.app.utils.linepay.LinePayClient;
import com.ruoyi.app.utils.linepay.LinePayCredential;
import com.ruoyi.app.utils.linepay.LinePayOfflineRequest;
import com.ruoyi.app.utils.linepay.LinePayResponse;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.system.domain.InfoUser;
import com.ruoyi.system.domain.PosOrder;
import com.ruoyi.system.domain.PosOrderLinePayment;
import com.ruoyi.system.domain.PosStore;
import com.ruoyi.system.domain.PosStoreLinePay;
import com.ruoyi.system.service.IInfoUserService;
import com.ruoyi.system.service.IPosOrderLinePaymentService;
import com.ruoyi.system.service.IPosOrderService;
import com.ruoyi.system.service.IPosStoreService;
import com.ruoyi.system.service.IPosStoreLinePayService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;

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

/** Merchant-presented LINE Pay Offline My Code orchestration. */
@Service
public class LinePayOfflineService {
    private static final Logger log = LoggerFactory.getLogger(LinePayOfflineService.class);
    private static final String MODE_OFFLINE = "OFFLINE";
    private static final Set<String> AUTH_REQUIRED_CODES = Set.of("1145", "1169");
    private static final Set<String> UNCERTAIN_CODES = Set.of(
            "1152", "1172", "1198", "1199", "9000");
    private static final Set<String> DEFINITE_PAY_FAILURE_CODES = Set.of(
            "1101", "1102", "1104", "1105", "1106", "1110", "1124", "1133",
            "1141", "1142");

    @Value("${line-pay.reconcile.auth-deadline-minutes:30}")
    private long authDeadlineMinutes = 30L;
    @Value("${line-pay.reconcile.unknown-deadline-hours:24}")
    private long unknownDeadlineHours = 24L;

    private final IPosOrderService orderService;
    private final IInfoUserService userService;
    private final IPosStoreService storeService;
    private final IPosStoreLinePayService credentialService;
    private final IPosOrderLinePaymentService paymentService;
    private final LinePayClient linePayClient;
    private final LinePayFactService factService;
    private final LinePayGatewayAuditService auditService;
    private final PaymentCreateGuardService createGuard;

    public LinePayOfflineService(IPosOrderService orderService,
                                 IInfoUserService userService,
                                 IPosStoreService storeService,
                                 IPosStoreLinePayService credentialService,
                                 IPosOrderLinePaymentService paymentService,
                                 LinePayClient linePayClient,
                                 LinePayFactService factService) {
        this(orderService, userService, storeService, credentialService, paymentService, linePayClient,
                factService, null, null);
    }

    @Autowired
    public LinePayOfflineService(IPosOrderService orderService,
                                 IInfoUserService userService,
                                 IPosStoreService storeService,
                                 IPosStoreLinePayService credentialService,
                                 IPosOrderLinePaymentService paymentService,
                                 LinePayClient linePayClient,
                                 LinePayFactService factService,
                                 LinePayGatewayAuditService auditService,
                                 PaymentCreateGuardService createGuard) {
        this.orderService = orderService;
        this.userService = userService;
        this.storeService = storeService;
        this.credentialService = credentialService;
        this.paymentService = paymentService;
        this.linePayClient = linePayClient;
        this.factService = factService;
        this.auditService = auditService;
        this.createGuard = createGuard;
    }

    public LinePayOfflineResult pay(Long merchantId, String ddId, String oneTimeKey) {
        if (oneTimeKey == null || !oneTimeKey.matches("\\d{18}")) {
            throw new ServiceException(MessageUtils.message("line.pay.offline.mycode.invalid"));
        }
        if (createGuard == null) {
            return payUnderLock(merchantId, normalizeDdId(ddId), oneTimeKey);
        }
        String normalized = normalizeDdId(ddId);
        return createGuard.withLock(normalized,
                () -> payUnderLock(merchantId, normalized, oneTimeKey));
    }

    private LinePayOfflineResult payUnderLock(Long merchantId, String ddId, String oneTimeKey) {
        PosOrder order = requireMerchantOrder(merchantId, ddId, true);
        PosOrderLinePayment active = paymentService.getActiveByDdId(ddId);
        if (active != null) {
            if (!MODE_OFFLINE.equals(active.getPaymentMode())) {
                throw new ServiceException(MessageUtils.message("line.pay.offline.attempt.blocked"));
            }
            // 已有活跃流水：本次扫码不再发给 LINE，前端凭 reusedAttempt 提示不可重复提交
            log.info("LINE Pay offline pay reuses active attempt, ddId={}, paymentId={}, status={}",
                    ddId, active.getId(), active.getStatus());
            return result(active, true);
        }
        PosStoreLinePay credential = credentialService.getEnabledCurrent(order.getMdId());
        if (credential == null) {
            throw new ServiceException(MessageUtils.message("line.pay.offline.not.enabled"));
        }
        IntentSelection selection = createIntent(order, credential);
        PosOrderLinePayment intent = selection.payment();
        if (!selection.created() || !"REQUESTING".equals(intent.getStatus())) {
            log.info("LINE Pay offline pay returns existing attempt, ddId={}, paymentId={}, status={}",
                    ddId, intent.getId(), intent.getStatus());
            return result(intent, true);
        }
        LinePayCredential gatewayCredential = toCredential(credential);
        // 发往 LINE 前留下完整上下文（金额/流水/付款码掩码），网关超时排查依赖这一行
        log.info("LINE Pay offline gateway pay start, ddId={}, paymentId={}, lineOrderId={}, storeId={}, amount={}, currency={}, myCode={}",
                ddId, intent.getId(), intent.getLineOrderId(), intent.getStoreId(), intent.getAmount(),
                intent.getCurrency(), maskMyCode(oneTimeKey));
        try {
            LinePayOfflineRequest request = new LinePayOfflineRequest(intent.getLineOrderId(),
                    ddId, "Order " + ddId, intent.getAmount(), intent.getCurrency(), oneTimeKey);
            LinePayResponse response = auditService == null
                    ? linePayClient.payOffline(gatewayCredential, request)
                    : auditService.executeOffline("APP", intent.getId(), credential.getId(),
                    intent.getStoreId(), intent.getDdId(), intent.getLineOrderId(),
                    () -> linePayClient.payOffline(gatewayCredential, request));
            return handlePayResponse(intent, response);
        } catch (Exception unknown) {
            // 网关调用结果未知（超时/断网）：异常在此被吞并转 REQUEST_UNKNOWN，无日志则完全无法事后排查
            log.error("LINE Pay offline gateway pay ended unknown, ddId={}, paymentId={}, lineOrderId={}",
                    ddId, intent.getId(), intent.getLineOrderId(), unknown);
            int changed = paymentService.markRequestUnknown(intent.getId(), intent.getVersion(),
                    shortly(), unknownDeadline());
            if (changed != 1) {
                return result(reload(intent), false);
            }
            intent.setStatus("REQUEST_UNKNOWN");
            intent.setUpdateTime(new Date());
            return result(intent, false);
        }
    }

    public LinePayOfflineResult status(Long merchantId, String ddId) {
        PosOrder order = requireMerchantOrder(merchantId, normalizeDdId(ddId), false);
        PosOrderLinePayment selected = paymentService.getByDdId(String.valueOf(order.getDdId())).stream()
                .filter(payment -> MODE_OFFLINE.equals(payment.getPaymentMode()))
                .max(Comparator.comparing(PosOrderLinePayment::getCreateTime,
                                Comparator.nullsFirst(Date::compareTo))
                        .thenComparing(PosOrderLinePayment::getId,
                                Comparator.nullsFirst(Long::compareTo)))
                .orElseThrow(() -> new ServiceException(
                        MessageUtils.message("line.pay.offline.attempt.not.found")));
        return result(selected, null);
    }

    public String reconcilePayment(Long paymentId, String source) {
        PosOrderLinePayment payment = paymentService.getById(paymentId);
        if (payment == null || !MODE_OFFLINE.equals(payment.getPaymentMode())) {
            throw new ServiceException(MessageUtils.message("line.pay.offline.attempt.not.found"));
        }
        PosStoreLinePay credential = credentialService.getById(payment.getCredentialId());
        if (credential == null) {
            // 凭证被删后查单无从发起，静默转人工会让问题不可见
            log.warn("LINE Pay offline credential gone to manual review, ddId={}, paymentId={}, credentialId={}",
                    payment.getDdId(), payment.getId(), payment.getCredentialId());
            int changed = paymentService.markManualReview(
                    payment.getId(), payment.getVersion(), payment.getStatus());
            return changed == 1 ? "MANUAL_REVIEW" : reload(payment).getStatus();
        }
        boolean expired = payment.getReconcileDeadline() != null
                && !new Date().before(payment.getReconcileDeadline());
        log.info("LINE Pay offline reconcile start, source={}, paymentId={}, ddId={}, status={}, expired={}",
                source, paymentId, payment.getDdId(), payment.getStatus(), expired);
        try {
            LinePayCredential gatewayCredential = toCredential(credential);
            LinePayResponse response = auditService == null
                    ? linePayClient.checkOffline(gatewayCredential, payment.getLineOrderId())
                    : auditService.execute("CHECK", source == null ? "TASK" : source,
                    payment.getId(), null, credential.getId(), payment.getStoreId(),
                    payment.getDdId(), payment.getLineOrderId(), payment.getTransactionId(),
                    () -> linePayClient.checkOffline(gatewayCredential, payment.getLineOrderId()));
            return handleCheckResponse(payment, response, expired);
        } catch (Exception unknown) {
            // 查单异常被吞并改期/转人工，不记录则对账停摆无从发现
            log.error("LINE Pay offline reconcile query failed, source={}, paymentId={}, ddId={}, expired={}",
                    source, paymentId, payment.getDdId(), expired, unknown);
            if (expired) {
                int changed = paymentService.markManualReview(
                        payment.getId(), payment.getVersion(), payment.getStatus());
                return changed == 1 ? "MANUAL_REVIEW" : reload(payment).getStatus();
            }
            int changed = paymentService.rescheduleOffline(
                    payment.getId(), payment.getVersion(), payment.getStatus(),
                    shortly(), payment.getReconcileDeadline() == null
                            ? unknownDeadline() : payment.getReconcileDeadline());
            return changed == 1 ? payment.getStatus() : reload(payment).getStatus();
        }
    }

    private LinePayOfflineResult handlePayResponse(PosOrderLinePayment payment,
                                                    LinePayResponse response) {
        if (response != null && AUTH_REQUIRED_CODES.contains(response.returnCode())) {
            int changed = paymentService.markOfflineWaitingAuth(payment.getId(), payment.getVersion(),
                    response.transactionId(), shortly(), waitingAuthDeadline(payment));
            if (changed != 1) {
                return result(reload(payment), false);
            }
            payment.setTransactionId(response.transactionId());
            payment.setStatus("WAITING_AUTH");
            payment.setUpdateTime(new Date());
            log.info("LINE Pay offline pay waiting auth, ddId={}, paymentId={}, returnCode={}, transactionId={}",
                    payment.getDdId(), payment.getId(), response.returnCode(), response.transactionId());
            return result(payment, false);
        }
        if (response != null && response.isSuccess()) {
            return applyCompletedFact(payment, response);
        }
        if (isDefinitePayFailure(response)) {
            int changed = paymentService.markTerminal(payment.getId(), payment.getVersion(),
                    payment.getStatus(), "FAILED");
            if (changed != 1) {
                return result(reload(payment), false);
            }
            payment.setStatus("FAILED");
            payment.setActiveDdId(null);
            payment.setUpdateTime(new Date());
            log.warn("LINE Pay offline pay definite failure, ddId={}, paymentId={}, httpStatus={}, returnCode={}, returnMessage={}",
                    payment.getDdId(), payment.getId(), response.httpStatus(), response.returnCode(),
                    response.returnMessage());
            return result(payment, false);
        }
        int changed = paymentService.markRequestUnknown(payment.getId(), payment.getVersion(),
                shortly(), unknownDeadline());
        if (changed != 1) {
            return result(reload(payment), false);
        }
        payment.setStatus("REQUEST_UNKNOWN");
        payment.setUpdateTime(new Date());
        log.warn("LINE Pay offline pay result uncertain, ddId={}, paymentId={}, httpStatus={}, returnCode={}, returnMessage={}",
                payment.getDdId(), payment.getId(), response.httpStatus(), response.returnCode(),
                response.returnMessage());
        return result(payment, false);
    }

    private String handleCheckResponse(PosOrderLinePayment payment, LinePayResponse response,
                                       boolean expired) {
        if (response == null || !response.isSuccess()) {
            return rescheduleOrReview(payment, expired);
        }
        JSONObject info = response.info();
        String gatewayStatus = info == null ? null : info.getString("status");
        if (gatewayStatus == null && info != null) {
            gatewayStatus = info.getString("paymentStatus");
        }
        if ("AUTH_READY".equals(gatewayStatus) || "WAITING_AUTH".equals(gatewayStatus)) {
            if (expired) {
                int changed = paymentService.markManualReview(
                        payment.getId(), payment.getVersion(), payment.getStatus());
                if (changed == 1) {
                    log.warn("LINE Pay offline auth expired to manual review, ddId={}, paymentId={}",
                            payment.getDdId(), payment.getId());
                }
                return changed == 1 ? "MANUAL_REVIEW" : reload(payment).getStatus();
            }
            int changed = paymentService.markOfflineWaitingAuth(payment.getId(), payment.getVersion(),
                    response.transactionId(), shortly(), waitingAuthDeadline(payment));
            if (changed == 1) {
                // 与 rescheduled 同属对账常态路径（60 秒一轮），debug 避免刷屏
                log.debug("LINE Pay offline reconcile still waiting auth, ddId={}, paymentId={}, gatewayStatus={}",
                        payment.getDdId(), payment.getId(), gatewayStatus);
            }
            return changed == 1 ? "WAITING_AUTH" : reload(payment).getStatus();
        }
        if ("COMPLETE".equals(gatewayStatus)) {
            return applyCompletedFact(payment, response).getStatus();
        }
        if ("CANCEL".equals(gatewayStatus)) {
            int changed = paymentService.markTerminal(payment.getId(), payment.getVersion(),
                    payment.getStatus(), "CANCELLED_OR_EXPIRED");
            if (changed == 1) {
                log.info("LINE Pay offline payment cancelled at gateway, ddId={}, paymentId={}, transactionId={}",
                        payment.getDdId(), payment.getId(), response.transactionId());
            }
            return changed == 1 ? "CANCELLED_OR_EXPIRED" : reload(payment).getStatus();
        }
        if ("FAIL".equals(gatewayStatus)) {
            int changed = paymentService.markTerminal(payment.getId(), payment.getVersion(),
                    payment.getStatus(), "FAILED");
            if (changed == 1) {
                log.warn("LINE Pay offline payment failed at gateway, ddId={}, paymentId={}, transactionId={}",
                        payment.getDdId(), payment.getId(), response.transactionId());
            }
            return changed == 1 ? "FAILED" : reload(payment).getStatus();
        }
        return rescheduleOrReview(payment, expired);
    }

    private LinePayOfflineResult applyCompletedFact(PosOrderLinePayment payment,
                                                    LinePayResponse response) {
        JSONObject info = response.info();
        String transactionId = info == null ? null : info.getString("transactionId");
        String orderId = info == null ? null : info.getString("orderId");
        Integer actualAmount = paymentAmount(info);
        String provider = paymentProvider(info);
        if (transactionId == null || transactionId.isBlank()) {
            int changed = paymentService.markManualReview(
                    payment.getId(), payment.getVersion(), payment.getStatus());
            if (changed != 1) {
                return result(reload(payment), false);
            }
            payment.setStatus("MANUAL_REVIEW");
            log.warn("LINE Pay offline complete without transactionId, ddId={}, paymentId={}, lineOrderId={}",
                    payment.getDdId(), payment.getId(), payment.getLineOrderId());
            return result(payment, false);
        }
        if (!payment.getLineOrderId().equals(orderId) || actualAmount == null
                || provider == null || provider.isBlank()) {
            if (actualAmount == null) {
                int changed = paymentService.markManualReview(
                        payment.getId(), payment.getVersion(), payment.getStatus());
                if (changed != 1) {
                    return result(reload(payment), false);
                }
                payment.setStatus("MANUAL_REVIEW");
                return result(payment, false);
            }
            int changed = paymentService.markOfflineEvidence(payment.getId(), payment.getVersion(),
                    payment.getStatus(), "MANUAL_REVIEW", transactionId, provider,
                    actualAmount, new Date());
            if (changed != 1) {
                return result(reload(payment), false);
            }
            payment.setTransactionId(transactionId);
            payment.setPaymentProvider(provider);
            payment.setCapturedAmount(actualAmount);
            payment.setStatus("MANUAL_REVIEW");
            log.warn("LINE Pay offline evidence mismatch to manual review, ddId={}, paymentId={}, lineOrderId={}, gatewayOrderId={}, captured={}, transactionId={}",
                    payment.getDdId(), payment.getId(), payment.getLineOrderId(), orderId,
                    actualAmount, transactionId);
            return result(payment, false);
        }
        if (!payment.getAmount().equals(actualAmount)) {
            boolean changed = factService.applyAmountMismatchFact(payment.getId(),
                    payment.getVersion(), payment.getStatus(), transactionId, provider,
                    actualAmount, new Date());
            if (!changed) {
                return result(reload(payment), false);
            }
            payment.setTransactionId(transactionId);
            payment.setPaymentProvider(provider);
            payment.setCapturedAmount(actualAmount);
            payment.setStatus("AMOUNT_MISMATCH");
            log.warn("LINE Pay offline amount mismatch, ddId={}, paymentId={}, expected={}, captured={}, transactionId={}",
                    payment.getDdId(), payment.getId(), payment.getAmount(), actualAmount, transactionId);
            return result(payment, false);
        }
        factService.applyPaidFact(payment.getId(), transactionId, provider, new Date());
        payment.setTransactionId(transactionId);
        payment.setPaymentProvider(provider);
        payment.setCapturedAmount(payment.getAmount());
        payment.setStatus("PAID");
        payment.setUpdateTime(new Date());
        log.info("LINE Pay offline payment PAID, ddId={}, paymentId={}, transactionId={}, provider={}, amount={}",
                payment.getDdId(), payment.getId(), transactionId, provider, payment.getAmount());
        return result(payment, false);
    }

    private String rescheduleOrReview(PosOrderLinePayment payment, boolean expired) {
        if (expired) {
            int changed = paymentService.markManualReview(
                    payment.getId(), payment.getVersion(), payment.getStatus());
            if (changed == 1) {
                log.warn("LINE Pay offline reconcile expired to manual review, ddId={}, paymentId={}, status={}",
                        payment.getDdId(), payment.getId(), payment.getStatus());
            }
            return changed == 1 ? "MANUAL_REVIEW" : reload(payment).getStatus();
        }
        int changed = paymentService.rescheduleOffline(
                payment.getId(), payment.getVersion(), payment.getStatus(),
                shortly(), payment.getReconcileDeadline() == null
                        ? unknownDeadline() : payment.getReconcileDeadline());
        if (changed == 1) {
            // 改期是常态路径（每分钟对账一轮都会触发），放 debug 避免刷屏
            log.debug("LINE Pay offline reconcile rescheduled, ddId={}, paymentId={}, status={}",
                    payment.getDdId(), payment.getId(), payment.getStatus());
        }
        return changed == 1 ? payment.getStatus() : reload(payment).getStatus();
    }

    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", MODE_OFFLINE, unknownDeadline()), true);
            } catch (DuplicateKeyException duplicate) {
                // 活跃流水唯一键冲突 = 并发扫码，后到者复用先到者流水
                log.info("LINE Pay offline intent duplicate key, ddId={}, attempt={}", order.getDdId(), attempt);
                PosOrderLinePayment active = paymentService.getActiveByDdId(
                        String.valueOf(order.getDdId()));
                if (active != null) {
                    if (!MODE_OFFLINE.equals(active.getPaymentMode())) {
                        throw new ServiceException(
                                MessageUtils.message("line.pay.offline.attempt.blocked"));
                    }
                    return new IntentSelection(active, false);
                }
                if (attempt == 2) {
                    throw duplicate;
                }
            }
        }
        throw new ServiceException(MessageUtils.message("line.pay.offline.operation.failed"));
    }

    private PosOrder requireMerchantOrder(Long merchantId, String ddId, boolean requirePayable) {
        if (merchantId == null) {
            throw new ServiceException(MessageUtils.message("line.pay.offline.merchant.required"));
        }
        InfoUser merchant = userService.getOne(new QueryWrapper<InfoUser>()
                .eq("user_id", merchantId));
        if (merchant == null || "0".equals(merchant.getUserType())
                || "2".equals(merchant.getUserType())) {
            throw new ServiceException(MessageUtils.message("line.pay.offline.merchant.required"));
        }
        PosOrder order = orderService.getOne(new QueryWrapper<PosOrder>().eq("dd_id", ddId));
        if (order == null || !"MERCHANT".equals(order.getOrderSource())) {
            // 三个 forbidden 抛点对商家展示同一消息，靠日志区分具体哪条不满足
            log.warn("LINE Pay offline order rejected: null or not merchant-sourced, merchantId={}, ddId={}, orderSource={}, shId={}, mdId={}",
                    merchantId, ddId, order == null ? null : order.getOrderSource(),
                    order == null ? null : order.getShId(), order == null ? null : order.getMdId());
            throw new ServiceException(MessageUtils.message("line.pay.offline.order.forbidden"));
        }
        boolean directMerchant = "1".equals(merchant.getUserType())
                || "3".equals(merchant.getUserType());
        PosStore store = storeService.getById(order.getMdId());
        if (store == null) {
            log.warn("LINE Pay offline order rejected: store missing, merchantId={}, ddId={}, mdId={}",
                    merchantId, ddId, order.getMdId());
            throw new ServiceException(MessageUtils.message("line.pay.offline.order.forbidden"));
        }
        boolean owned = directMerchant
                ? merchant.getUserId().equals(order.getShId())
                && merchant.getUserId().equals(store.getUserId())
                : merchant.getStoreId() != null && merchant.getStoreId().equals(order.getMdId());
        if (!owned) {
            log.warn("LINE Pay offline order rejected: not owned, merchantId={}, userType={}, merchantStoreId={}, ddId={}, orderShId={}, orderMdId={}, storeUserId={}",
                    merchantId, merchant.getUserType(), merchant.getStoreId(), ddId,
                    order.getShId(), order.getMdId(), store.getUserId());
            throw new ServiceException(MessageUtils.message("line.pay.offline.order.forbidden"));
        }
        String parentDdId = order.getParentDdId() == null
                ? String.valueOf(order.getDdId()) : order.getParentDdId();
        if (orderService.count(new QueryWrapper<PosOrder>()
                .eq("parent_dd_id", parentDdId)) > 1) {
            throw new ServiceException(MessageUtils.message("line.pay.offline.single.store.required"));
        }
        if (requirePayable && (!LinePayService.PAY_TYPE_LINE.equals(order.getPayType())
                || order.getPayStatus() != null && order.getPayStatus() != 0L
                || order.getState() == null || order.getState() == 3L || order.getState() == 4L
                || order.getAmount() == null || order.getAmount() <= 0 || order.getMdId() == null
                || order.getAfterSaleStatus() != null && order.getAfterSaleStatus() != 0L)) {
            log.warn("LINE Pay offline order rejected: not payable, merchantId={}, ddId={}, payType={}, payStatus={}, state={}, amount={}, afterSaleStatus={}",
                    merchantId, ddId, order.getPayType(), order.getPayStatus(), order.getState(),
                    order.getAmount(), order.getAfterSaleStatus());
            throw new ServiceException(MessageUtils.message("line.pay.offline.order.invalid"));
        }
        return order;
    }

    private static Integer paymentAmount(JSONObject info) {
        if (info == null) {
            return null;
        }
        JSONArray payInfo = info.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 paymentProvider(JSONObject info) {
        if (info == null) {
            return null;
        }
        String provider = info.getString("paymentProvider");
        if (provider != null && !provider.isBlank()) {
            return provider;
        }
        JSONArray payInfo = info.getJSONArray("payInfo");
        if (payInfo == null) {
            return null;
        }
        for (int i = 0; i < payInfo.size(); i++) {
            JSONObject item = payInfo.getJSONObject(i);
            String itemProvider = item == null ? null : item.getString("paymentProvider");
            if (itemProvider != null && !itemProvider.isBlank()) {
                return itemProvider;
            }
        }
        return null;
    }

    private static boolean isDefinitePayFailure(LinePayResponse response) {
        if (response == null || response.httpStatus() < 200 || response.httpStatus() >= 300
                || response.returnCode() == null || response.isSuccess()
                || AUTH_REQUIRED_CODES.contains(response.returnCode())
                || UNCERTAIN_CODES.contains(response.returnCode())
                || response.returnCode().startsWith("190")) {
            return false;
        }
        return DEFINITE_PAY_FAILURE_CODES.contains(response.returnCode());
    }

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

    /** 付款码掩码：仅保留后 4 位；完整 oneTimeKey 属一次性凭证，禁止写入日志。 */
    private static String maskMyCode(String oneTimeKey) {
        if (oneTimeKey == null || oneTimeKey.length() < 4) {
            return "****";
        }
        return "****" + oneTimeKey.substring(oneTimeKey.length() - 4);
    }

    private static String normalizeDdId(String ddId) {
        if (ddId == null || ddId.trim().isEmpty()) {
            throw new ServiceException(MessageUtils.message("line.pay.order.required"));
        }
        return ddId.trim();
    }

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

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

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

    private Date unknownDeadline() {
        return new Date(System.currentTimeMillis() + unknownDeadlineHours * 3_600_000L);
    }

    private Date waitingAuthDeadline(PosOrderLinePayment payment) {
        return "WAITING_AUTH".equals(payment.getStatus())
                && payment.getReconcileDeadline() != null
                ? payment.getReconcileDeadline() : authDeadline();
    }

    private PosOrderLinePayment reload(PosOrderLinePayment fallback) {
        PosOrderLinePayment current = paymentService.getById(fallback.getId());
        return current == null ? fallback : current;
    }

    private static LinePayOfflineResult result(PosOrderLinePayment payment, Boolean reused) {
        LinePayOfflineResult result = new LinePayOfflineResult();
        result.setDdId(payment.getDdId());
        result.setPaymentId(payment.getId());
        result.setLineOrderId(payment.getLineOrderId());
        result.setTransactionId(payment.getTransactionId());
        result.setStatus(publicStatus(payment.getStatus()));
        result.setReusedAttempt(reused);
        result.setUpdatedAt(payment.getUpdateTime());
        return result;
    }

    private static String publicStatus(String status) {
        if ("WAITING_AUTH".equals(status)) {
            return "AUTH_REQUIRED";
        }
        if ("REQUESTING".equals(status) || "REQUEST_UNKNOWN".equals(status)) {
            return "PROCESSING";
        }
        if ("CANCELLED_OR_EXPIRED".equals(status)) {
            return "CANCELLED";
        }
        if ("AMOUNT_MISMATCH".equals(status) || "MANUAL_REVIEW".equals(status)) {
            return "MANUAL_REVIEW";
        }
        return status;
    }

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