package com.ruoyi.app.pay;

import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.app.utils.linepay.LinePayClient;
import com.ruoyi.app.utils.linepay.LinePayCredential;
import com.ruoyi.app.utils.linepay.LinePayResponse;
import com.ruoyi.common.exception.ServiceException;
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.IPosStoreLinePayService;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;

import java.util.Date;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;

/** Executes idempotent full refunds and recovers uncertain refund outcomes by Retrieve. */
@Service
public class LinePayRefundService {
    private static final Set<String> RETRYABLE_CODES = Set.of("1900", "1902", "1999");
    private static final Set<String> UNCERTAIN_CODES = Set.of(
            "1164", "1165", "1198", "1199", "9000");
    private static final Set<String> CREDENTIAL_AUTH_FAILURE_CODES = Set.of("1104", "1105", "1106");

    private final IPosOrderLinePaymentService paymentService;
    private final IPosOrderLineRefundService refundService;
    private final IPosStoreLinePayService credentialService;
    private final LinePayClient linePayClient;
    private final LinePayFactService factService;
    private final LinePayGatewayAuditService auditService;
    @Value("${line-pay.reconcile.unknown-deadline-hours:24}")
    private long unknownDeadlineHours = 24L;
    @Value("${line-pay.reconcile.row-lease-seconds:120}")
    private long rowLeaseSeconds = 120L;

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

    @Autowired
    public LinePayRefundService(IPosOrderLinePaymentService paymentService,
                                IPosOrderLineRefundService refundService,
                                IPosStoreLinePayService credentialService,
                                LinePayClient linePayClient,
                                LinePayFactService factService,
                                LinePayGatewayAuditService auditService) {
        this.paymentService = paymentService;
        this.refundService = refundService;
        this.credentialService = credentialService;
        this.linePayClient = linePayClient;
        this.factService = factService;
        this.auditService = auditService;
    }

    public String requestFullRefund(Long paymentId, String source) {
        PosOrderLinePayment payment = paymentService.getById(paymentId);
        if (payment == null || !("PAID".equals(payment.getStatus())
                || "AMOUNT_MISMATCH".equals(payment.getStatus()))) {
            throw new ServiceException("Only a captured LINE Pay payment can be refunded");
        }
        PosOrderLineRefund refund = refundService.createIfAbsent(payment, source);
        if ("REFUNDED".equals(refund.getStatus())) {
            factService.applyRefundFact(payment.getId());
            return "REFUNDED";
        }
        if (refund.getReconcileDeadline() != null
                && !new Date().before(refund.getReconcileDeadline())) {
            if ("PROCESSING".equals(refund.getStatus())) {
                refundService.recoverExpiredProcessing(refund.getId(), refund.getVersion(),
                        shortly(), unknownDeadline());
                refund = refundService.getByPaymentId(paymentId);
            }
            if (refund == null) {
                throw new ServiceException("LINE Pay refund state requires reconciliation");
            }
            String recovered = recoverUnknown(payment, refund, credentialFor(payment), false);
            if ("REFUNDED".equals(recovered)) {
                return recovered;
            }
            if ("MANUAL_REVIEW".equals(recovered)) {
                return recovered;
            }
            return markManualReview(refund.getPaymentId(), refund.getId(), refund.getVersion(),
                    refund.getStatus());
        }
        LinePayCredential credential = credentialFor(payment);
        if ("PROCESSING".equals(refund.getStatus())) {
            if (refund.getLeaseUntil() != null && refund.getLeaseUntil().before(new Date())
                    && refundService.recoverExpiredProcessing(refund.getId(), refund.getVersion(),
                    shortly(), unknownDeadline()) == 1) {
                refund = refundService.getByPaymentId(paymentId);
            } else {
                return "PROCESSING";
            }
        }
        if ("UNKNOWN".equals(refund.getStatus())) {
            return recoverUnknown(payment, refund, credential);
        }
        if (!("CREATED".equals(refund.getStatus()) || "RETRY_WAIT".equals(refund.getStatus()))) {
            return refund.getStatus();
        }
        String owner = (source == null ? "LINE" : source) + "-" + UUID.randomUUID();
        if (refundService.claimProcessing(refund.getId(), refund.getVersion(), refund.getStatus(),
                owner, leaseUntil()) != 1) {
            return refund.getStatus();
        }
        long processingVersion = refund.getVersion() + 1;
        try {
            RefundEvidenceResult evidenceResult = retrieveRefundEvidence(
                    payment, refund, credential, source);
            RefundEvidence existingRefund = evidenceResult.evidence();
            LinePayCredential actionCredential = evidenceResult.credential();
            if (existingRefund.fullRefundTransactionId() != null) {
                factService.applyRefundedFact(refund.getId(), payment.getId(), processingVersion,
                        existingRefund.fullRefundTransactionId(), new Date());
                return "REFUNDED";
            }
            if (existingRefund.hasAnyRefund()) {
                return markManualReview(refund.getPaymentId(), refund.getId(), processingVersion,
                        "PROCESSING");
            }
            if (!existingRefund.definitelyNotRefunded()) {
                refundService.markUnknown(refund.getId(), processingVersion, shortly(), unknownDeadline());
                return "UNKNOWN";
            }
            LinePayResponse response = gatewayCall("REFUND", source, payment, refund,
                    actionCredential.id(), () -> "OFFLINE".equals(payment.getPaymentMode())
                            ? linePayClient.refundOfflineFull(actionCredential,
                            payment.getLineOrderId())
                            : linePayClient.refundFull(actionCredential,
                            payment.getTransactionId()));
            String refundTransactionId = response.info() == null
                    ? null : response.info().getString("refundTransactionId");
            if (response.isSuccess() && refundTransactionId != null
                    ) {
                factService.applyRefundedFact(refund.getId(), payment.getId(), processingVersion,
                        refundTransactionId, new Date());
                return "REFUNDED";
            }
            if (response != null && RETRYABLE_CODES.contains(response.returnCode())) {
                refundService.markRetryWait(refund.getId(), processingVersion, shortly());
                return "RETRY_WAIT";
            }
            if (response != null && response.httpStatus() >= 200 && response.httpStatus() < 300
                    && response.returnCode() != null
                    && !UNCERTAIN_CODES.contains(response.returnCode())) {
                refundService.markFailed(refund.getId(), processingVersion);
                return "FAILED";
            }
        } catch (Exception ignored) {
            // The gateway may have accepted the request; only Retrieve may decide its outcome.
        }
        refundService.markUnknown(refund.getId(), processingVersion, shortly(), unknownDeadline());
        return "UNKNOWN";
    }

    private LinePayCredential credentialFor(PosOrderLinePayment payment) {
        PosStoreLinePay credentialVersion = credentialService.getById(payment.getCredentialId());
        if (credentialVersion == null) {
            throw new ServiceException("LINE Pay credential version not found");
        }
        return new LinePayCredential(credentialVersion.getId(), credentialVersion.getChannelId(),
                credentialVersion.getChannelSecret());
    }

    /** Creates the durable idempotent intent; the scheduled worker performs the gateway call. */
    public String createRefundIntent(Long paymentId, String source) {
        PosOrderLinePayment payment = paymentService.getById(paymentId);
        if (payment == null || !("PAID".equals(payment.getStatus())
                || "AMOUNT_MISMATCH".equals(payment.getStatus()))) {
            throw new ServiceException("Only a captured LINE Pay payment can be refunded");
        }
        return refundService.createIfAbsent(payment, source).getStatus();
    }

    private String recoverUnknown(PosOrderLinePayment payment, PosOrderLineRefund refund,
                                  LinePayCredential credential) {
        return recoverUnknown(payment, refund, credential, true);
    }

    private String recoverUnknown(PosOrderLinePayment payment, PosOrderLineRefund refund,
                                  LinePayCredential credential, boolean reschedule) {
        try {
            RefundEvidence evidence = retrieveRefundEvidence(
                    payment, refund, credential, "TASK").evidence();
            if (evidence.fullRefundTransactionId() != null) {
                factService.applyRefundedFact(refund.getId(), payment.getId(), refund.getVersion(),
                        evidence.fullRefundTransactionId(), new Date());
                return "REFUNDED";
            }
            if (evidence.hasAnyRefund()) {
                return markManualReview(refund.getPaymentId(), refund.getId(), refund.getVersion(),
                        refund.getStatus());
            }
        } catch (Exception ignored) {
            // Keep UNKNOWN; a later scheduled Retrieve can recover it.
        }
        if (reschedule) {
            Date deadline = refund.getReconcileDeadline() == null
                    ? unknownDeadline() : refund.getReconcileDeadline();
            refundService.rescheduleUnknown(refund.getId(), refund.getVersion(), shortly(), deadline);
        }
        return "UNKNOWN";
    }

    private String markManualReview(Long paymentId, Long refundId, Long version,
                                    String expectedStatus) {
        if (refundService.markManualReview(refundId, version, expectedStatus) == 1) {
            return "MANUAL_REVIEW";
        }
        PosOrderLineRefund current = refundService.getByPaymentId(paymentId);
        if (current == null) {
            throw new ServiceException("LINE Pay refund state requires reconciliation");
        }
        if ("MANUAL_REVIEW".equals(current.getStatus()) || "REFUNDED".equals(current.getStatus())) {
            return current.getStatus();
        }
        if (("UNKNOWN".equals(current.getStatus()) || "PROCESSING".equals(current.getStatus()))
                && refundService.markManualReview(current.getId(), current.getVersion(),
                current.getStatus()) == 1) {
            return "MANUAL_REVIEW";
        }
        PosOrderLineRefund latest = refundService.getByPaymentId(paymentId);
        return latest == null ? current.getStatus() : latest.getStatus();
    }

    private RefundEvidenceResult retrieveRefundEvidence(PosOrderLinePayment payment,
                                                         PosOrderLineRefund refund,
                                                         LinePayCredential credential,
                                                         String source) throws Exception {
        LinePayResponse retrieve = gatewayCall("RETRIEVE", source, payment, refund,
                credential.id(),
                () -> linePayClient.retrieveByTransactionId(credential, payment.getTransactionId()));
        RefundEvidence evidence = refundEvidence(retrieve, payment, refund);
        if (!isCredentialAuthFailure(retrieve)) {
            return new RefundEvidenceResult(evidence, credential);
        }
        PosStoreLinePay original = credentialService.getById(payment.getCredentialId());
        PosStoreLinePay current = credentialService.getCurrent(payment.getStoreId());
        if (original == null || current == null || Objects.equals(current.getId(), original.getId())
                || !Objects.equals(current.getChannelId(), original.getChannelId())
                || !Objects.equals(current.getEnvironment(), original.getEnvironment())) {
            return new RefundEvidenceResult(evidence, credential);
        }
        LinePayCredential currentCredential = new LinePayCredential(current.getId(),
                current.getChannelId(), current.getChannelSecret());
        LinePayResponse proof = gatewayCall("RETRIEVE", source, payment, refund,
                currentCredential.id(),
                () -> linePayClient.retrieveByTransactionId(
                        currentCredential, payment.getTransactionId()));
        RefundEvidence currentEvidence = refundEvidence(proof, payment, refund);
        return currentEvidence.conclusive()
                ? new RefundEvidenceResult(currentEvidence, currentCredential)
                : new RefundEvidenceResult(evidence, credential);
    }

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

    private static RefundEvidence refundEvidence(LinePayResponse response,
                                                 PosOrderLinePayment expected,
                                                 PosOrderLineRefund refund) {
        if (response == null || !response.isSuccess() || response.rawBody() == null) {
            return RefundEvidence.INCONCLUSIVE;
        }
        Integer expectedCapturedAmount = refund == null ? null : refund.getAmount();
        if (expectedCapturedAmount == null || expectedCapturedAmount <= 0) {
            return RefundEvidence.INCONCLUSIVE;
        }
        JSONArray info = JSONObject.parseObject(response.rawBody()).getJSONArray("info");
        if (info == null || info.size() != 1) {
            return RefundEvidence.INCONCLUSIVE;
        }
        JSONObject payment = info.getJSONObject(0);
        if (payment == null || !expected.getTransactionId().equals(payment.getString("transactionId"))
                || !expected.getLineOrderId().equals(payment.getString("orderId"))
                || !"PAYMENT".equals(payment.getString("transactionType"))
                || !expected.getCurrency().equals(payment.getString("currency"))
                || !expectedCapturedAmount.equals(paymentAmount(payment))) {
            return RefundEvidence.INCONCLUSIVE;
        }
        JSONArray refunds = payment.getJSONArray("refundList");
        if (refunds == null || refunds.isEmpty()) {
            return RefundEvidence.DEFINITELY_NOT_REFUNDED;
        }
        long refundedAmount = 0L;
        String lastRefundTransactionId = null;
        for (int i = 0; i < refunds.size(); i++) {
            JSONObject item = refunds.getJSONObject(i);
            if (item == null) {
                return RefundEvidence.INCONCLUSIVE;
            }
            String refundTransactionId = item.getString("refundTransactionId");
            Integer amount = item.getInteger("refundAmount");
            if (refundTransactionId == null || amount == null || amount == 0) {
                return RefundEvidence.INCONCLUSIVE;
            }
            refundedAmount += Math.abs((long) amount);
            lastRefundTransactionId = refundTransactionId;
        }
        return new RefundEvidence(true,
                refundedAmount == expectedCapturedAmount ? lastRefundTransactionId : null, false);
    }

    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 record RefundEvidence(boolean hasAnyRefund, String fullRefundTransactionId,
                                  boolean definitelyNotRefunded) {
        private static final RefundEvidence DEFINITELY_NOT_REFUNDED =
                new RefundEvidence(false, null, true);
        private static final RefundEvidence INCONCLUSIVE = new RefundEvidence(false, null, false);

        private boolean conclusive() {
            return hasAnyRefund || definitelyNotRefunded;
        }
    }

    private record RefundEvidenceResult(RefundEvidence evidence, LinePayCredential credential) {
    }

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

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

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

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

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