package com.jeesharp.business.pay.service;

import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jeesharp.business.modules.finance.api.PayOrderApi;
import com.jeesharp.business.modules.finance.api.PayRecordApi;
import com.jeesharp.business.modules.finance.api.support.WechatPayNotifyParseSupport;
import com.jeesharp.business.modules.finance.paymentrecord.enums.PayRecordBizSceneEnum;
import com.jeesharp.business.pay.adapter.MobilePayBizType;
import com.jeesharp.business.pay.adapter.WechatPayCallbackAdapter;
import com.jeesharp.business.modules.pay.api.PayCenterApi;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * 微信支付回调统一适配层：验签与解密由 {@link PayCenterApi#wechatParsePayNotify} 完成；本类负责
 * 字段解析、业务适配器路由与财务流水记录。
 *
 * <p>与微信支付 V3「支付成功回调通知」对齐：外层 {@code event_type} 为 {@code TRANSACTION.SUCCESS}
 * 时才执行业务确认；验签通过后由 Controller 尽快应答 HTTP 204（无包体），避免回调超时重试。</p>
 *
 * @see <a href="https://pay.weixin.qq.com/doc/v3/merchant/4012791861">支付成功回调通知</a>
 * @author jeesharp
 * @since 2026-05-08
 */
@Slf4j
@Service
public class WechatPayNotifyAdapterService {

    private final PayCenterApi payCenterApi;
    private final ObjectMapper objectMapper;
    private final PayRecordApi payRecordApi;
    private final PayOrderApi payOrderApi;
    /** 各业态 WechatPayCallbackAdapter 注册表，Spring 注入 List 后按 MobilePayBizType 索引 */
    private final Map<MobilePayBizType, WechatPayCallbackAdapter> adapters =
        new EnumMap<>(MobilePayBizType.class);

    public WechatPayNotifyAdapterService(PayCenterApi payCenterApi,
                                         ObjectMapper objectMapper,
                                         PayRecordApi payRecordApi,
                                         PayOrderApi payOrderApi,
                                         List<WechatPayCallbackAdapter> adapterList) {
        this.payCenterApi = payCenterApi;
        this.objectMapper = objectMapper;
        this.payRecordApi = payRecordApi;
        this.payOrderApi = payOrderApi;
        for (WechatPayCallbackAdapter adapter : adapterList) {
            adapters.put(adapter.bizType(), adapter);
        }
    }

    private static final String WECHAT_EVENT_TRANSACTION_SUCCESS = "TRANSACTION.SUCCESS";

    /**
     * 微信支付 V3 异步通知：按 URL 固定业态（遗留路径兼容）。
     */
    public void handleNotify(MobilePayBizType bizType, String rawBody, Map<String, String> headers) throws Exception {
        doHandle(bizType, rawBody, headers);
    }

    /**
     * 微信支付 V3 异步通知：从 attach.bizType 解析业态（杏花村 C 端统一回调入口）。
     */
    public void handleNotifyByAttach(String rawBody, Map<String, String> headers) throws Exception {
        doHandle(null, rawBody, headers);
    }

    /**
     * 支付回调核心编排：验签解密 → 事件过滤 → 合单/单笔分流 → 业态路由 → 业务确认 → 财务落账。
     *
     * @param urlBizType URL 路径携带的业态（遗留各业态独立回调地址）；统一入口传 null，改从 attach.bizType 解析
     */
    private void doHandle(MobilePayBizType urlBizType, String rawBody, Map<String, String> headers) throws Exception {
        // ① 验签 + AES 解密：委托 PayCenterApi，失败直接抛异常由 Controller 应答非 2xx
        Map<String, Object> body = payCenterApi.wechatParsePayNotify(rawBody, headers);
        log.info("Wechat pay notify parsed, urlBizType={}, body={}", urlBizType, body);

        // ② 仅处理「支付成功」事件，退款/关单等其他 event_type 直接忽略（微信仍会收到 204）
        String eventType = stringValue(body.get("event_type"));
        if (!eventType.isEmpty() && !WECHAT_EVENT_TRANSACTION_SUCCESS.equals(eventType)) {
            log.info("Wechat pay notify skip non-success event_type={}", eventType);
            return;
        }

        // ③ 合单支付分支：存在 combine_out_trade_no + sub_orders 时走整单确认（如商城多商户合单）
        Map<String, Object> decrypted = decryptedMap(body);
        List<Map<String, Object>> combineSubOrders = extractCombineSubOrders(decrypted);
        String combineOutTradeNo = decrypted == null ? "" : stringValue(decrypted.get("combine_out_trade_no"));
        if (StrUtil.isNotBlank(combineOutTradeNo) && !combineSubOrders.isEmpty()) {
            Map<String, Object> combineAttach = extractAttachFromSubOrders(combineSubOrders);
            if (combineAttach.isEmpty()) {
                combineAttach = extractAttach(body);
            }
            MobilePayBizType combineBizType = resolveBizType(urlBizType, combineAttach);
            handleCombineNotify(combineBizType, rawBody, body, combineOutTradeNo, combineSubOrders);
            return;
        }

        // ④ 普通 JSAPI 单笔：从 attach 与解密报文提取业务主键
        Map<String, Object> attachMap = extractAttach(body);
        String orderId = extractOrderId(body, attachMap);
        String payOrderId = extractPayOrderId(body, attachMap);
        String thirdPartyTradeNo = extractThirdPartyTradeNo(body);
        String outTradeNo = extractOutTradeNo(body);
        String tradeState = extractTradeState(body);
        MobilePayBizType bizType = resolveBizType(urlBizType, attachMap);

        // ⑤ 按业态路由到对应 WechatPayCallbackAdapter（订餐系列），调用 confirmPaySuccess 类用例
        WechatPayCallbackAdapter adapter = adapters.get(bizType);
        if (adapter == null) {
            throw new IllegalStateException("未找到支付回调适配器: " + bizType);
        }
        // ⑥ trade_state=SUCCESS 且能解析出业务订单号时：回写业务单状态 + 记财务流水（幂等）
        if ("SUCCESS".equals(tradeState) && StrUtil.isNotBlank(orderId)) {
            adapter.confirm(orderId, payOrderId, thirdPartyTradeNo);
            recordFinancePayNotify(bizType, rawBody, body, attachMap, orderId, outTradeNo, thirdPartyTradeNo);
        }
    }

    /**
     * 解析回调业态：优先 URL 路径（遗留）；统一入口则从预下单 attach.bizType 反查枚举。
     * URL 与 attach 同时存在时会交叉校验，防止伪造 attach 跨业态确认。
     */
    private MobilePayBizType resolveBizType(MobilePayBizType urlBizType, Map<String, Object> attachMap) {
        if (urlBizType != null) {
            validateBizType(urlBizType, attachMap);
            return urlBizType;
        }
        String attachBizType = stringValue(attachMap.get("bizType"));
        if (StrUtil.isBlank(attachBizType)) {
            throw new IllegalArgumentException("微信支付回调 attach.bizType 缺失");
        }
        for (MobilePayBizType candidate : MobilePayBizType.values()) {
            if (candidate.getCode().equals(attachBizType)) {
                return candidate;
            }
        }
        throw new IllegalArgumentException("微信支付回调 attach.bizType 无法识别: " + attachBizType);
    }

    /**
     * 合单支付成功通知：所有子单 trade_state 均为 SUCCESS 后，按主单 orderId 整单确认一次并落财务流水。
     * <p>合单仅回调一次，业务侧 confirm 负责拆分子单状态；财务侧以 combine_out_trade_no 作为商户订单号。</p>
     */
    private void handleCombineNotify(MobilePayBizType bizType, String rawBody, Map<String, Object> body,
            String combineOutTradeNo, List<Map<String, Object>> subOrders) {
        // 子单未全部成功时不确认（部分成功/中间态等待后续通知或人工处理）
        if (!isCombineSubOrdersAllSuccess(subOrders)) {
            log.info("Wechat combine notify skip: not all sub_orders SUCCESS, combineOutTradeNo={}", combineOutTradeNo);
            return;
        }
        // attach 优先从任一子单读取（各子单 attach 应一致，含 orderId / payOrderId / bizType）
        Map<String, Object> attachMap = extractAttachFromSubOrders(subOrders);
        if (attachMap.isEmpty()) {
            attachMap = extractAttach(body);
        }
        validateBizType(bizType, attachMap);
        String orderId = stringValue(attachMap.get("orderId"));
        if (StrUtil.isBlank(orderId)) {
            log.warn("Wechat combine notify skip: missing orderId in attach, combineOutTradeNo={}", combineOutTradeNo);
            return;
        }
        String payOrderId = stringValue(attachMap.get("payOrderId"));
        // 微信侧流水号取首个非空子单 transaction_id（合单场景用于对账）
        String thirdPartyTradeNo = extractCombineThirdPartyTradeNo(subOrders);
        WechatPayCallbackAdapter adapter = adapters.get(bizType);
        if (adapter == null) {
            throw new IllegalStateException("未找到支付回调适配器: " + bizType);
        }
        // ⑥ 业务确认 + 财务落账（与业务 confirm 解耦，财务失败不影响 204）
        adapter.confirm(orderId, payOrderId, thirdPartyTradeNo);
        recordFinancePayNotify(bizType, rawBody, body, attachMap, orderId, combineOutTradeNo, thirdPartyTradeNo);
    }

    @SuppressWarnings("unchecked")
    private static List<Map<String, Object>> extractCombineSubOrders(Map<String, Object> decrypted) {
        if (decrypted == null) {
            return List.of();
        }
        Object raw = decrypted.get("sub_orders");
        if (!(raw instanceof List<?> list)) {
            return List.of();
        }
        List<Map<String, Object>> result = new ArrayList<>();
        for (Object item : list) {
            if (item instanceof Map<?, ?> map) {
                result.add((Map<String, Object>) map);
            }
        }
        return result;
    }

    private Map<String, Object> extractAttachFromSubOrders(List<Map<String, Object>> subOrders) {
        for (Map<String, Object> sub : subOrders) {
            if (sub == null) {
                continue;
            }
            String attach = stringValue(sub.get("attach"));
            if (StrUtil.isBlank(attach)) {
                continue;
            }
            try {
                return objectMapper.readValue(attach, LinkedHashMap.class);
            } catch (Exception e) {
                log.warn("Wechat combine sub_order attach parse failed: {}", e.getMessage());
            }
        }
        return Map.of();
    }

    private static boolean isCombineSubOrdersAllSuccess(List<Map<String, Object>> subOrders) {
        if (subOrders == null || subOrders.isEmpty()) {
            return false;
        }
        for (Map<String, Object> sub : subOrders) {
            if (sub == null || !"SUCCESS".equals(stringValue(sub.get("trade_state")))) {
                return false;
            }
        }
        return true;
    }

    private static String extractCombineThirdPartyTradeNo(List<Map<String, Object>> subOrders) {
        if (subOrders == null) {
            return "";
        }
        for (Map<String, Object> sub : subOrders) {
            if (sub == null) {
                continue;
            }
            String txn = stringValue(sub.get("transaction_id"));
            if (StrUtil.isNotBlank(txn)) {
                return txn;
            }
        }
        return "";
    }

    @SuppressWarnings("unchecked")
    private String extractOrderId(Map<String, Object> body, Map<String, Object> attachMap) {
        Map<String, Object> decrypted = (Map<String, Object>) body.get("resource_decrypted");
        if (decrypted == null) {
            return "";
        }
        String attachOrderId = stringValue(attachMap.get("orderId"));
        if (StrUtil.isNotBlank(attachOrderId)) {
            return attachOrderId;
        }
        String direct = stringValue(decrypted.get("orderId"));
        if (StrUtil.isNotBlank(direct)) {
            return direct;
        }
        return stringValue(decrypted.get("out_trade_no"));
    }

    @SuppressWarnings("unchecked")
    private static String extractPayOrderId(Map<String, Object> body, Map<String, Object> attachMap) {
        Map<String, Object> decrypted = (Map<String, Object>) body.get("resource_decrypted");
        if (decrypted == null) {
            return "";
        }
        String attachPayOrderId = stringValue(attachMap.get("payOrderId"));
        if (StrUtil.isNotBlank(attachPayOrderId)) {
            return attachPayOrderId;
        }
        String direct = stringValue(decrypted.get("payOrderId"));
        if (StrUtil.isNotBlank(direct)) {
            return direct;
        }
        return "";
    }

    @SuppressWarnings("unchecked")
    private Map<String, Object> extractAttach(Map<String, Object> body) {
        Map<String, Object> decrypted = (Map<String, Object>) body.get("resource_decrypted");
        if (decrypted == null) {
            return Map.of();
        }
        String attach = stringValue(decrypted.get("attach"));
        if (StrUtil.isBlank(attach)) {
            return Map.of();
        }
        try {
            return objectMapper.readValue(attach, LinkedHashMap.class);
        } catch (Exception e) {
            log.warn("Wechat pay notify attach parse failed: {}", e.getMessage());
            return Map.of();
        }
    }

    @SuppressWarnings("unchecked")
    private static String extractThirdPartyTradeNo(Map<String, Object> body) {
        Map<String, Object> decrypted = (Map<String, Object>) body.get("resource_decrypted");
        return decrypted == null ? "" : stringValue(decrypted.get("transaction_id"));
    }

    @SuppressWarnings("unchecked")
    private static String extractOutTradeNo(Map<String, Object> body) {
        Map<String, Object> decrypted = (Map<String, Object>) body.get("resource_decrypted");
        return decrypted == null ? "" : stringValue(decrypted.get("out_trade_no"));
    }

    @SuppressWarnings("unchecked")
    private static String extractTradeState(Map<String, Object> body) {
        Map<String, Object> decrypted = (Map<String, Object>) body.get("resource_decrypted");
        return decrypted == null ? "" : stringValue(decrypted.get("trade_state"));
    }

    /** URL 业态与 attach.bizType 不一致时拒绝处理，防止跨业态伪造回调。 */
    private static void validateBizType(MobilePayBizType bizType, Map<String, Object> attachMap) {
        String attachBizType = stringValue(attachMap.get("bizType"));
        if (StrUtil.isNotBlank(attachBizType) && !bizType.getCode().equals(attachBizType)) {
            throw new SecurityException("微信支付回调业务类型不匹配");
        }
    }

    private static String stringValue(Object value) {
        return value == null ? "" : String.valueOf(value);
    }

    /**
     * 财务侧落账（与业务 confirm 解耦，失败仅打日志不影响微信 204 应答）。
     * <ol>
     *   <li>更新统一支付单 {@link PayOrderApi} 为已支付</li>
     *   <li>幂等写入支付流水 {@link PayRecordApi}（供结算、对账、商户应收）</li>
     * </ol>
     */
    private void recordFinancePayNotify(MobilePayBizType bizType, String rawBody, Map<String, Object> body,
                                        Map<String, Object> attachMap, String orderId, String outTradeNo,
                                        String thirdPartyTradeNo) {
        try {
            Map<String, Object> decrypted = decryptedMap(body);
            // 从 attach / 解密报文提取会员、金额、支付时间等财务字段
            String memberId = stringValue(attachMap.get("memberId"));
            String subject = decrypted == null ? "" : stringValue(decrypted.get("description"));
            LocalDateTime payTime = WechatPayNotifyParseSupport.parseSuccessTime(
                    decrypted == null ? null : decrypted.get("success_time"));
            BigDecimal amountYuan = WechatPayNotifyParseSupport.parseTotalAmountYuan(decrypted);
            String bizScene = bizSceneCode(bizType);
            String attachPayOrderId = stringValue(attachMap.get("payOrderId"));
            // ① 更新统一支付单 biz_pay_order 为已支付
            // 优先按预下单生成的 payOrderId 关单；无则回退商户 out_trade_no（兼容旧链路）
            if (StrUtil.isNotBlank(attachPayOrderId)) {
                payOrderApi.markPaidByPayOrderId(attachPayOrderId, thirdPartyTradeNo, amountYuan, payTime);
            } else {
                payOrderApi.markPaidByWechatOutTradeNo(outTradeNo, thirdPartyTradeNo, amountYuan, payTime);
            }
            // ② 幂等写入支付流水（供结算、对账、商户应收）
            payRecordApi.recordWechatJsapiNotifyIfAbsent(
                    bizScene,
                    orderId,
                    outTradeNo,
                    thirdPartyTradeNo,
                    amountYuan,
                    StrUtil.isBlank(subject) ? null : subject,
                    StrUtil.isBlank(memberId) ? null : memberId,
                    payTime,
                    rawBody);
        } catch (Exception e) {
            log.error("Finance pay record save failed after wechat notify, bizType={}, orderId={}", bizType, orderId, e);
        }
    }

    /** 移动端业态枚举 → 财务流水 biz_scene 字典（结算 SPI、商户应收按场景分账）。 */
    private static String bizSceneCode(MobilePayBizType bizType) {
        return switch (bizType) {
            case FOOD -> PayRecordBizSceneEnum.FOOD_ORDER.getCode();
            case FOOD_MEAL -> PayRecordBizSceneEnum.FOOD_MEAL_ORDER.getCode();
            case FOOD_COMPANY_MEAL -> PayRecordBizSceneEnum.FOOD_COMPANY_MEAL_ORDER.getCode();
        };
    }

    @SuppressWarnings("unchecked")
    private static Map<String, Object> decryptedMap(Map<String, Object> body) {
        Object v = body.get("resource_decrypted");
        return v instanceof Map ? (Map<String, Object>) v : null;
    }
}
