package com.ruoyi.app.omgpay;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.app.order.DeliveryOrderNotificationService;
import com.ruoyi.app.order.MerchantNotificationRouter;
import com.ruoyi.app.order.dto.OrderPushBodyDto;
import com.ruoyi.app.omgpay.dto.OmgNotifyRequest;
import com.ruoyi.system.domain.PosOrder;
import com.ruoyi.system.omgpay.domain.OmgPaymentAttempt;
import com.ruoyi.system.omgpay.domain.OmgPaymentOrderSnapshot;
import com.ruoyi.system.omgpay.service.IOmgPaymentAttemptService;
import com.ruoyi.system.service.IPosOrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Date;
import java.util.List;

/** Verifies and applies one final OMG payment notification transactionally. */
@Service
public class OmgPaymentNotifyService {
    private static final Logger log = LoggerFactory.getLogger(OmgPaymentNotifyService.class);
    private static final int STATUS_PAID = 1;
    private static final int STATUS_SUPERSEDED = 3;
    private static final DateTimeFormatter OMG_DATE = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
    private static final ZoneId TAIPEI = ZoneId.of("Asia/Taipei");
    private static final List<String> REQUIRED_FIELDS = List.of(
            "MerchantID", "MerchantTradeNo", "StoreID", "RtnCode", "RtnMsg", "TradeNo", "TradeAmt",
            "PaymentDate", "PaymentType", "PaymentTypeChargeFee", "TradeDate", "SimulatePaid",
            "CustomField1", "CustomField2", "CustomField3", "CustomField4", "CheckMacValue");

    private final IOmgPaymentAttemptService attempts;
    private final OmgCheckMacSigner signer;
    @Autowired(required = false)
    private IPosOrderService posOrderService;
    @Autowired(required = false)
    private DeliveryOrderNotificationService deliveryOrderNotificationService;
    @Autowired(required = false)
    private MerchantNotificationRouter merchantNotificationRouter;

    public OmgPaymentNotifyService(IOmgPaymentAttemptService attempts, OmgCheckMacSigner signer) {
        this.attempts = attempts;
        this.signer = signer;
    }

    @Transactional(rollbackFor = Exception.class)
    public boolean process(OmgNotifyRequest request) {
        if (request == null || !request.isValid() || !hasRequiredFields(request)) {
            log.warn("OMG notify rejected reason=malformed_or_missing_fields");
            return false;
        }
        String merchantTradeNo = request.value("MerchantTradeNo");
        if (isBlank(merchantTradeNo) || merchantTradeNo.length() > 20) {
            log.warn("OMG notify rejected reason=invalid_merchant_trade_no");
            return false;
        }

        OmgPaymentAttempt discovered = attempts.selectByMerchantTradeNo(merchantTradeNo);
        if (discovered == null) {
            log.warn("OMG notify rejected merchantTradeNo={} reason=attempt_not_found", merchantTradeNo);
            return false;
        }
        if (factsDoNotIdentifySameAttempt(request, discovered)) {
            log.warn("OMG notify rejected merchantTradeNo={} reason=prelock_fact_mismatch", merchantTradeNo);
            return false;
        }
        OmgPaymentOrderSnapshot order = attempts.selectOrderForUpdate(discovered.getDdId());
        OmgPaymentAttempt attempt = attempts.selectByMerchantTradeNoForUpdate(merchantTradeNo);
        if (attempt == null || !verifyTrust(request, attempt)) {
            log.warn("OMG notify rejected merchantTradeNo={} reason=trust_validation_failed", merchantTradeNo);
            return false;
        }

        ParsedFacts facts;
        try {
            facts = parseFacts(request);
        } catch (IllegalArgumentException exception) {
            log.warn("OMG notify rejected merchantTradeNo={} reason=invalid_gateway_facts", merchantTradeNo);
            return false;
        }
        OmgPaymentGatewayFacts gatewayFacts = new OmgPaymentGatewayFacts(
                OmgPaymentFactSource.NOTIFY, attempt.getMerchantId(), merchantTradeNo,
                attempt.getAmount(), facts.rtnCode, facts.rtnMsg, facts.tradeNo,
                facts.paymentType, facts.paymentDate, facts.tradeDate,
                facts.paymentTypeChargeFee, facts.simulatePaid);
        return applyVerifiedFacts(order, attempt, gatewayFacts) != null;
    }

    /**
     * Applies a signed QueryTradeInfo result so a lost callback cannot leave a paid order unpaid.
     * The row locks and irreversible state rules are identical to the callback path.
     */
    @Transactional(rollbackFor = Exception.class)
    public OmgPaymentSettlementResult synchronizeVerifiedQuery(OmgPaymentGatewayFacts facts) {
        if (facts == null || facts.source() != OmgPaymentFactSource.QUERY) {
            throw new IllegalArgumentException("verified OMG query facts are required");
        }
        OmgPaymentAttempt discovered = attempts.selectByMerchantTradeNo(facts.merchantTradeNo());
        if (discovered == null || !sameIdentity(facts, discovered)) {
            throw new IllegalArgumentException("OMG query facts do not identify an attempt");
        }
        OmgPaymentOrderSnapshot order = attempts.selectOrderForUpdate(discovered.getDdId());
        OmgPaymentAttempt attempt = attempts.selectByMerchantTradeNoForUpdate(facts.merchantTradeNo());
        if (attempt == null || !sameIdentity(facts, attempt)) {
            throw new IllegalArgumentException("OMG query attempt changed while locking");
        }
        return applyVerifiedFacts(order, attempt, facts);
    }

    private OmgPaymentSettlementResult applyVerifiedFacts(OmgPaymentOrderSnapshot order,
                                                          OmgPaymentAttempt attempt,
                                                          OmgPaymentGatewayFacts facts) {
        boolean paid = facts.resultCode() == 1;
        if (!paid) {
            if (attempt.getAttemptStatus() != null && attempt.getAttemptStatus() == STATUS_PAID) {
                log.warn("OMG {} ignored late failure merchantTradeNo={}, code={}, message={}",
                        facts.source(), attempt.getMerchantTradeNo(), facts.resultCode(), facts.resultMessage());
                return OmgPaymentSettlementResult.PAID;
            }
            if (attempt.getAttemptStatus() != null && attempt.getAttemptStatus() == STATUS_SUPERSEDED) {
                log.warn("OMG {} accepted failure for superseded attempt merchantTradeNo={}, code={}",
                        facts.source(), attempt.getMerchantTradeNo(), facts.resultCode());
                return OmgPaymentSettlementResult.FAILED;
            }
            requireSingleUpdate(attempts.markFailed(toUpdate(attempt, facts)), "mark failed");
            log.warn("OMG payment failed source={}, merchantTradeNo={}, tradeNo={}, code={}, message={}",
                    facts.source(), attempt.getMerchantTradeNo(), facts.tradeNo(),
                    facts.resultCode(), facts.resultMessage());
            return OmgPaymentSettlementResult.FAILED;
        }

        if (order == null) {
            throw new IllegalStateException("OMG paid fact has no order");
        }
        if (attempt.getAttemptStatus() != null && attempt.getAttemptStatus() == STATUS_PAID) {
            if (attempt.getTradeNo() != null && !attempt.getTradeNo().equals(facts.tradeNo())) {
                throw new IllegalStateException("OMG paid fact conflicts with stored TradeNo");
            }
            log.info("OMG paid duplicate accepted source={}, merchantTradeNo={}, tradeNo={}",
                    facts.source(), attempt.getMerchantTradeNo(), facts.tradeNo());
            return OmgPaymentSettlementResult.PAID;
        }

        requireSingleUpdate(attempts.markPaid(toUpdate(attempt, facts)), "mark paid");
        attempts.supersedeOtherCreated(attempt.getDdId(), attempt.getId());
        if (order.getPayStatus() == null || order.getPayStatus() != 1L) {
            requireSingleUpdate(attempts.markOrderPaid(attempt.getDdId()), "mark order paid");
            openDeliveryOrderToRiders(attempt.getDdId(), order.getState());
        }
        int otherPaid = attempts.countOtherPaidAttempts(attempt.getDdId(), attempt.getId());
        if (order.getState() != null && order.getState() == 4L) {
            log.error("OMG paid after order cancellation source={}, orderId={}, merchantTradeNo={}, tradeNo={}",
                    facts.source(), attempt.getDdId(), attempt.getMerchantTradeNo(), facts.tradeNo());
        }
        if (otherPaid > 0) {
            log.error("OMG multiple paid attempts source={}, orderId={}, merchantTradeNo={}, "
                            + "tradeNo={}, otherPaidCount={}",
                    facts.source(), attempt.getDdId(), attempt.getMerchantTradeNo(),
                    facts.tradeNo(), otherPaid);
        }
        log.info("OMG payment marked paid source={}, orderId={}, merchantTradeNo={}, tradeNo={}, amount={}",
                facts.source(), attempt.getDdId(), attempt.getMerchantTradeNo(),
                facts.tradeNo(), attempt.getAmount());
        return OmgPaymentSettlementResult.PAID;
    }

    private void openDeliveryOrderToRiders(String ddId, Long state) {
        if (Long.valueOf(4L).equals(state) || posOrderService == null
                || (deliveryOrderNotificationService == null && merchantNotificationRouter == null)) {
            return;
        }
        PosOrder paidOrder = posOrderService.getOne(new LambdaQueryWrapper<PosOrder>()
                .eq(PosOrder::getDdId, ddId));
        if (paidOrder == null) {
            return;
        }
        paidOrder.setPayStatus(1L);
        if (Long.valueOf(0L).equals(paidOrder.getType()) && deliveryOrderNotificationService != null) {
            deliveryOrderNotificationService.notifyOrderAvailable(paidOrder);
        } else if (merchantNotificationRouter != null) {
            String orderNo = String.valueOf(paidOrder.getDdId());
            String body = OrderPushBodyDto.getJson(orderNo, String.valueOf(paidOrder.getState()), 0);
            merchantNotificationRouter.sendStoreNotification(paidOrder.getMdId(), paidOrder.getShId(),
                    "no.message.push.message", "no.message.push.new.order", body, orderNo);
        }
    }

    private boolean verifyTrust(OmgNotifyRequest request, OmgPaymentAttempt attempt) {
        if (!request.value("MerchantID").equals(attempt.getMerchantId())) {
            return false;
        }
        Integer amount = parseInteger(request.value("TradeAmt"));
        if (amount == null || !amount.equals(attempt.getAmount())) {
            return false;
        }
        String actual = request.value("CheckMacValue");
        if (actual == null || !actual.matches("(?i)[0-9a-f]{64}")) {
            return false;
        }
        String expected = signer.sign(request.signingFields(),
                attempt.getHashKeySnapshot(), attempt.getHashIvSnapshot());
        return secureEquals(expected, actual);
    }

    private static boolean factsDoNotIdentifySameAttempt(OmgNotifyRequest request, OmgPaymentAttempt attempt) {
        Integer amount = parseInteger(request.value("TradeAmt"));
        return !request.value("MerchantID").equals(attempt.getMerchantId())
                || amount == null || !amount.equals(attempt.getAmount());
    }

    private static boolean hasRequiredFields(OmgNotifyRequest request) {
        return REQUIRED_FIELDS.stream().allMatch(request::contains);
    }

    private static ParsedFacts parseFacts(OmgNotifyRequest request) {
        Integer rtnCode = requiredInteger(request.value("RtnCode"));
        BigDecimal fee = requiredDecimal(request.value("PaymentTypeChargeFee"));
        if (fee.signum() < 0) {
            throw new IllegalArgumentException("invalid PaymentTypeChargeFee");
        }
        Integer simulatePaid = requiredInteger(request.value("SimulatePaid"));
        if (simulatePaid != 0 && simulatePaid != 1) {
            throw new IllegalArgumentException("invalid SimulatePaid");
        }
        String tradeNo = request.value("TradeNo");
        if ((rtnCode == 1 && isBlank(tradeNo)) || (tradeNo != null && tradeNo.length() > 20)) {
            throw new IllegalArgumentException("invalid TradeNo");
        }
        if (isBlank(tradeNo)) {
            tradeNo = null;
        }
        String paymentType = request.value("PaymentType");
        if (isBlank(paymentType) || paymentType.length() > 20) {
            throw new IllegalArgumentException("invalid PaymentType");
        }
        Date paymentDate = parseDate(request.value("PaymentDate"), rtnCode == 1);
        Date tradeDate = parseDate(request.value("TradeDate"), true);
        String rtnMsg = request.value("RtnMsg");
        if (rtnMsg == null || rtnMsg.length() > 200) {
            throw new IllegalArgumentException("invalid RtnMsg");
        }
        return new ParsedFacts(rtnCode, rtnMsg, tradeNo, paymentType,
                paymentDate, tradeDate, fee, simulatePaid);
    }

    private static OmgPaymentAttempt toUpdate(OmgPaymentAttempt attempt, OmgPaymentGatewayFacts facts) {
        OmgPaymentAttempt update = new OmgPaymentAttempt();
        update.setId(attempt.getId());
        update.setTradeNo(facts.tradeNo());
        update.setRtnCode(facts.resultCode());
        update.setRtnMsg(limit(facts.resultMessage(), 200));
        update.setPaymentType(limit(facts.paymentType(), 20));
        update.setPaymentDate(facts.paymentDate());
        update.setTradeDate(facts.tradeDate());
        update.setPaymentTypeChargeFee(facts.paymentTypeChargeFee());
        update.setSimulatePaid(facts.simulatePaid());
        update.setLastNotifyTime(new Date());
        update.setUpdateTime(new Date());
        return update;
    }

    private static boolean sameIdentity(OmgPaymentGatewayFacts facts, OmgPaymentAttempt attempt) {
        return facts.merchantTradeNo().equals(attempt.getMerchantTradeNo())
                && facts.merchantId().equals(attempt.getMerchantId())
                && facts.amount() == attempt.getAmount();
    }

    static boolean secureEquals(String expected, String actual) {
        return expected != null && actual != null
                && MessageDigest.isEqual(expected.toUpperCase().getBytes(StandardCharsets.US_ASCII),
                actual.toUpperCase().getBytes(StandardCharsets.US_ASCII));
    }

    private static Date parseDate(String value, boolean required) {
        if (isBlank(value)) {
            if (required) {
                throw new IllegalArgumentException("required date missing");
            }
            return null;
        }
        try {
            return Date.from(LocalDateTime.parse(value, OMG_DATE).atZone(TAIPEI).toInstant());
        } catch (DateTimeParseException exception) {
            throw new IllegalArgumentException("invalid date", exception);
        }
    }

    private static Integer requiredInteger(String value) {
        Integer parsed = parseInteger(value);
        if (parsed == null) {
            throw new IllegalArgumentException("invalid integer");
        }
        return parsed;
    }

    private static BigDecimal requiredDecimal(String value) {
        if (isBlank(value)) {
            throw new IllegalArgumentException("invalid decimal");
        }
        try {
            return new BigDecimal(value);
        } catch (NumberFormatException exception) {
            throw new IllegalArgumentException("invalid decimal", exception);
        }
    }

    private static Integer parseInteger(String value) {
        try {
            return isBlank(value) ? null : Integer.valueOf(value);
        } catch (NumberFormatException exception) {
            return null;
        }
    }

    private static void requireSingleUpdate(int count, String action) {
        if (count != 1) {
            throw new IllegalStateException("OMG notify failed to " + action);
        }
    }

    private static boolean isBlank(String value) {
        return value == null || value.isBlank();
    }

    private static String limit(String value, int length) {
        return value == null || value.length() <= length ? value : value.substring(0, length);
    }

    private record ParsedFacts(int rtnCode, String rtnMsg, String tradeNo, String paymentType,
                               Date paymentDate, Date tradeDate,
                               BigDecimal paymentTypeChargeFee, int simulatePaid) {
    }
}
