package com.liquidnet.service.goblin.service.impl;

import com.google.gson.Gson;
import com.kuaidi100.sdk.response.*;
import com.liquidnet.commons.lang.util.DateUtil;
import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.goblin.dto.vo.*;
import com.liquidnet.service.goblin.express.kuaidi100.Kuaidi100Client;
import com.liquidnet.service.goblin.service.IGoblinMailTrackService;
import com.liquidnet.service.goblin.util.GoblinMongoUtils;
import com.liquidnet.service.goblin.util.GoblinRedisUtils;
import com.liquidnet.service.goblin.enums.GoblinStoreConf;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.CompletableFuture;

/**
 * 快递100物流：订阅 + 主动查询 + Redis 双层缓存
 */
@Slf4j
@Service
public class GoblinMailTrackServiceImpl implements IGoblinMailTrackService {

    private static final int MAX_SUBSCRIBE_PER_MONTH = 4;
    private static final DateTimeFormatter MONTH_FMT = DateTimeFormatter.ofPattern("yyyy-MM");
    private static final Gson GSON = new Gson();

    private static final Map<String, String> STATE_TEXT = new HashMap<>();
    static {
        STATE_TEXT.put("0", "在途");
        STATE_TEXT.put("1", "揽收");
        STATE_TEXT.put("2", "疑难");
        STATE_TEXT.put("3", "签收");
        STATE_TEXT.put("4", "退签");
        STATE_TEXT.put("5", "派件");
        STATE_TEXT.put("6", "退回");
        STATE_TEXT.put("7", "转投");
        STATE_TEXT.put("8", "清关");
        STATE_TEXT.put("10", "待清关");
        STATE_TEXT.put("11", "清关中");
        STATE_TEXT.put("12", "已清关");
        STATE_TEXT.put("13", "清关异常");
        STATE_TEXT.put("14", "拒签");
    }

    @Autowired
    private Kuaidi100Client kuaidi100Client;
    @Autowired
    private GoblinRedisUtils redisUtils;
    @Autowired
    private GoblinMongoUtils mongoUtils;
    @Autowired
    private MongoTemplate mongoTemplate;

    @Value("${liquidnet.express.kuaidi100.queryDebounceSeconds:60}")
    private long queryDebounceSeconds;

    @Override
    public void initAndSubscribeAsync(String orderId, String mailId, String mailNo, String logisticsCode, String logisticsCompany) {
        CompletableFuture.runAsync(() -> {
            try {
                doInitAndSubscribe(orderId, mailId, mailNo, logisticsCode, logisticsCompany);
            } catch (Exception e) {
                log.error("[mailTrack] initAndSubscribeAsync error orderId={}, mailId={}", orderId, mailId, e);
            }
        });
    }

    private void doInitAndSubscribe(String orderId, String mailId, String mailNo, String logisticsCode, String logisticsCompany) {
        GoblinMailTrackCache cache = redisUtils.getMailTrackCache(mailId);
        if (cache == null) {
            cache = newEmptyCache(orderId, mailId);
        }
        GoblinMailVo mailVo = findMail(orderId, mailId);
        if (mailVo == null) {
            mailVo = GoblinMailVo.getNew();
            mailVo.setMailId(mailId);
            mailVo.setOrderId(orderId);
            mailVo.setMailNo(mailNo);
            mailVo.setLogisticsCode(logisticsCode);
            mailVo.setLogisticsCompany(logisticsCompany);
        } else {
            if (StringUtils.isNotBlank(logisticsCode) && StringUtils.isBlank(mailVo.getLogisticsCode())) {
                mailVo.setLogisticsCode(logisticsCode);
            }
            if (StringUtils.isNotBlank(logisticsCompany) && StringUtils.isBlank(mailVo.getLogisticsCompany())) {
                mailVo.setLogisticsCompany(logisticsCompany);
            }
        }
        String com = resolveAndPersistCom(orderId, mailVo);
        if (StringUtils.isBlank(com)) {
            updateSubscribeFail(cache, "无法识别快递公司编码");
            redisUtils.setMailTrackCache(mailId, cache);
            return;
        }
        ensureMonthCounter(cache.getSubscribe());
        if (Boolean.TRUE.equals(cache.getSubscribe().getQuotaExceeded())
                || (cache.getSubscribe().getSubscribeCount() != null && cache.getSubscribe().getSubscribeCount() >= MAX_SUBSCRIBE_PER_MONTH)) {
            cache.getSubscribe().setQuotaExceeded(true);
            cache.getSubscribe().setSubscribeStatus("quota_exceeded");
            cache.getSubscribe().setNeedResubscribe(false);
            redisUtils.setMailTrackCache(mailId, cache);
            queryAndOverwriteTrack(orderId, mailId, com, mailNo, "query");
            return;
        }
        doSubscribe(orderId, mailId, mailNo, com, cache);
        // 订阅成功也立刻主动查一次，避免等推送才有 track
        queryAndOverwriteTrack(orderId, mailId, com, mailNo, "query");
    }

    @Override
    public GoblinMailTrackCache resolveTrackForRead(String orderId, String mailId) {
        GoblinMailVo mailVo = findMail(orderId, mailId);
        if (mailVo == null) {
            return null;
        }
        // 统一用包裹真实 mailId（兼容误传 mailNo）
        mailId = mailVo.getMailId();
        GoblinMailTrackCache cache = redisUtils.getMailTrackCache(mailId);
        if (cache == null || cache.getSubscribe() == null) {
            cache = newEmptyCache(orderId, mailId);
        }
        GoblinMailTrackSubscribeVo sub = cache.getSubscribe();
        ensureMonthCounter(sub);

        boolean finished = "shutdown".equals(sub.getSubscribeStatus())
                || (cache.getTrack() != null && "1".equals(cache.getTrack().getIscheck()));
        // 已结束且已有轨迹：只读
        if (finished && hasTrackData(cache)) {
            return cache;
        }

        String com = resolveAndPersistCom(orderId, mailVo);
        if (StringUtils.isBlank(com)) {
            updateSubscribeFail(cache, "无法识别快递公司编码");
            redisUtils.setMailTrackCache(mailId, cache);
            return cache;
        }

        boolean overQuota = Boolean.TRUE.equals(sub.getQuotaExceeded())
                || (sub.getSubscribeCount() != null && sub.getSubscribeCount() >= MAX_SUBSCRIBE_PER_MONTH);
        if (overQuota) {
            sub.setQuotaExceeded(true);
        }

        // 订阅与查轨迹分开：已订阅则不再订；未订阅/需重订且未超限才订
        boolean needSubscribe = !finished && !overQuota
                && (!Boolean.TRUE.equals(sub.getSubscribed()) || Boolean.TRUE.equals(sub.getNeedResubscribe()));
        if (needSubscribe) {
            doSubscribe(orderId, mailId, mailVo.getMailNo(), com, cache);
            cache = redisUtils.getMailTrackCache(mailId);
            if (cache == null) {
                cache = newEmptyCache(orderId, mailId);
            }
            sub = cache.getSubscribe();
        }

        // 无轨迹（或超限只能靠主动查）则 QueryTrackMap
        if (!hasTrackData(cache) || overQuota) {
            queryAndOverwriteTrack(orderId, mailId, com, mailVo.getMailNo(), "query");
            GoblinMailTrackCache latest = redisUtils.getMailTrackCache(mailId);
            return latest != null ? latest : cache;
        }
        return cache;
    }

    /** 是否已有可用轨迹点（仅有 subscribe、track 为空不算） */
    private boolean hasTrackData(GoblinMailTrackCache cache) {
        return cache != null
                && cache.getTrack() != null
                && !CollectionUtils.isEmpty(cache.getTrack().getData());
    }

    @Override
    public void fillMailSummary(GoblinMailVo mailVo, GoblinMailTrackCache cache) {
        if (mailVo == null || cache == null || cache.getTrack() == null) {
            return;
        }
        GoblinMailTrackInfoVo track = cache.getTrack();
        mailVo.setState(track.getState());
        mailVo.setStateText(STATE_TEXT.getOrDefault(track.getState(), track.getState()));
        mailVo.setTrailUrl(track.getTrailUrl());
        mailVo.setIscheck(track.getIscheck());
        if (track.getData() != null && !track.getData().isEmpty()) {
            GoblinMailTrackDataVo latest = track.getData().get(0);
            mailVo.setLatestContext(latest.getContext());
            mailVo.setLatestTime(StringUtils.isNotBlank(latest.getFtime()) ? latest.getFtime() : latest.getTime());
            if (StringUtils.isBlank(mailVo.getStateText()) && StringUtils.isNotBlank(latest.getStatus())) {
                mailVo.setStateText(latest.getStatus());
            }
        }
    }

    @Override
    public Map<String, Object> handleCallback(String param, String sign, String mailId, String orderId) {
        Map<String, Object> resp = new HashMap<>();
        resp.put("result", true);
        resp.put("returnCode", "200");
        resp.put("message", "成功");
        try {
            if (!kuaidi100Client.verifyCallbackSign(param, sign)) {
                log.warn("[mailTrack] callback sign mismatch mailId={}", mailId);
                // 仍返回成功避免无限重试；业务侧可查日志
            }
            SubscribeWithMapPushParamResp push = GSON.fromJson(param, SubscribeWithMapPushParamResp.class);
            if (push == null) {
                return resp;
            }
            if (StringUtils.isBlank(mailId) && push.getLastResult() != null) {
                // fallback: cannot resolve without mailId in callback url
                log.warn("[mailTrack] callback missing mailId, nu={}", push.getLastResult().getNu());
                return resp;
            }
            // 改单号后 mailId 不变，旧订阅仍可能推送：仅接受与当前 mailNo 一致的推送
            String pushNu = push.getLastResult() != null ? push.getLastResult().getNu() : null;
            GoblinMailVo currentMail = findMail(orderId, mailId);
            if (currentMail != null && StringUtils.isNotBlank(currentMail.getMailNo())
                    && StringUtils.isNotBlank(pushNu)
                    && !currentMail.getMailNo().trim().equalsIgnoreCase(pushNu.trim())) {
                log.warn("[mailTrack] callback mailNo mismatch, ignore. mailId={}, currentMailNo={}, pushNu={}",
                        mailId, currentMail.getMailNo(), pushNu);
                return resp;
            }
            GoblinMailTrackCache cache = redisUtils.getMailTrackCache(mailId);
            if (cache == null) {
                cache = newEmptyCache(orderId, mailId);
            }
            String status = push.getStatus();
            GoblinMailTrackSubscribeVo sub = cache.getSubscribe();
            if ("abort".equals(status)) {
                sub.setSubscribeStatus("abort");
                sub.setNeedResubscribe(true);
                sub.setSubscribed(false);
                sub.setLastFailReason(push.getMessage());
                sub.setLastFailAt(nowStr());
                if (StringUtils.isNotBlank(push.getComNew()) && cache.getTrack() != null) {
                    cache.getTrack().setCom(push.getComNew());
                }
            } else if ("shutdown".equals(status)) {
                sub.setSubscribeStatus("shutdown");
                sub.setNeedResubscribe(false);
                sub.setSubscribed(true);
            } else if ("polling".equals(status) || "updateall".equals(status)) {
                sub.setSubscribeStatus("polling");
                sub.setNeedResubscribe(false);
                sub.setSubscribed(true);
            }
            if (push.getLastResult() != null) {
                GoblinMailTrackInfoVo track = fromPushResult(push.getLastResult(), "subscribe");
                cache.setTrack(track);
            }
            redisUtils.setMailTrackCache(mailId, cache);
        } catch (Exception e) {
            log.error("[mailTrack] handleCallback error mailId={}", mailId, e);
        }
        return resp;
    }

    @Override
    public ResponseDto<GoblinMailTrackCache> adminGetTrack(String orderId, String mailId) {
        if (!assertStoreOrder(orderId)) {
            return ResponseDto.failure("无法查看");
        }
        GoblinMailVo mailVo = findMail(orderId, mailId);
        if (mailVo == null) {
            return ResponseDto.failure("快递信息不存在");
        }
        GoblinMailTrackCache cache = redisUtils.getMailTrackCache(mailId);
        if (cache == null) {
            cache = newEmptyCache(orderId, mailId);
        }
        return ResponseDto.success(cache);
    }

    @Override
    public ResponseDto<GoblinMailTrackInfoVo> adminSyncTrack(String orderId, String mailId) {
        if (!assertStoreOrder(orderId)) {
            return ResponseDto.failure("无法查看");
        }
        GoblinMailVo mailVo = findMail(orderId, mailId);
        if (mailVo == null) {
            return ResponseDto.failure("快递信息不存在");
        }
        String com = resolveAndPersistCom(orderId, mailVo);
        if (StringUtils.isBlank(com)) {
            return ResponseDto.failure("无法识别快递公司编码");
        }
        GoblinMailTrackInfoVo track = queryAndOverwriteTrack(orderId, mailId, com, mailVo.getMailNo(), "admin");
        if (track == null) {
            return ResponseDto.failure("查询物流失败");
        }
        return ResponseDto.success(track);
    }

    @Override
    public ResponseDto<Boolean> adminResubscribe(String orderId, String mailId) {
        if (!assertStoreOrder(orderId)) {
            return ResponseDto.failure("无法查看");
        }
        GoblinMailVo mailVo = findMail(orderId, mailId);
        if (mailVo == null) {
            return ResponseDto.failure("快递信息不存在");
        }
        GoblinMailTrackCache cache = redisUtils.getMailTrackCache(mailId);
        if (cache == null) {
            cache = newEmptyCache(orderId, mailId);
        }
        ensureMonthCounter(cache.getSubscribe());
        if (Boolean.TRUE.equals(cache.getSubscribe().getQuotaExceeded())
                || (cache.getSubscribe().getSubscribeCount() != null && cache.getSubscribe().getSubscribeCount() >= MAX_SUBSCRIBE_PER_MONTH)) {
            return ResponseDto.failure("本月订阅次数已达上限，请使用同步物流");
        }
        if ("shutdown".equals(cache.getSubscribe().getSubscribeStatus())) {
            return ResponseDto.failure("运单已签收结束，无需重新订阅");
        }
        String com = resolveAndPersistCom(orderId, mailVo);
        if (StringUtils.isBlank(com)) {
            return ResponseDto.failure("无法识别快递公司编码");
        }
        boolean ok = doSubscribe(orderId, mailId, mailVo.getMailNo(), com, cache);
        if (ok) {
            queryAndOverwriteTrack(orderId, mailId, com, mailVo.getMailNo(), "admin");
        }
        return ok ? ResponseDto.success(true) : ResponseDto.failure("重新订阅失败：" +
                Optional.ofNullable(redisUtils.getMailTrackCache(mailId))
                        .map(GoblinMailTrackCache::getSubscribe)
                        .map(GoblinMailTrackSubscribeVo::getLastFailReason)
                        .orElse("未知错误"));
    }

    private boolean assertStoreOrder(String orderId) {
        String uid = com.liquidnet.commons.lang.util.CurrentUtil.getCurrentUid();
        GoblinStoreInfoVo storeInfoVo = redisUtils.getStoreInfoVoByUid(uid);
        if (storeInfoVo == null) {
            return false;
        }
        GoblinStoreOrderVo orderVo = redisUtils.getGoblinOrder(orderId);
        return orderVo != null && storeInfoVo.getStoreId().equals(orderVo.getStoreId());
    }

    @Override
    public ResponseDto<List<GoblinStoreRecommendGoodsVo>> recommendStoreGoods(String orderId) {
        GoblinStoreOrderVo orderVo = redisUtils.getGoblinOrder(orderId);
        if (orderVo == null) {
            return ResponseDto.failure("订单不存在");
        }
        String storeId = orderVo.getStoreId();
        Set<String> excludeSpuIds = new HashSet<>();
        if (orderVo.getOrderSkuVoIds() != null) {
            for (String orderSkuId : orderVo.getOrderSkuVoIds()) {
                GoblinOrderSkuVo skuVo = redisUtils.getGoblinOrderSkuVo(orderSkuId);
                if (skuVo != null && StringUtils.isNotBlank(skuVo.getSpuId())) {
                    excludeSpuIds.add(skuVo.getSpuId());
                }
            }
        }

        boolean hideSoldOut = true;
        List<GoblinStoreConfigVo> configVos = redisUtils.getStoreConfigVos(storeId);
        if (!CollectionUtils.isEmpty(configVos)) {
            for (GoblinStoreConfigVo conf : configVos) {
                if (GoblinStoreConf.ONOFF_SOLD_OUT_SHOW.name().equals(conf.getConfigKey()) && "ON".equals(conf.getConfigVal())) {
                    hideSoldOut = false;
                }
            }
        }

        Query query = new Query();
        query.addCriteria(Criteria.where("storeId").is(storeId)
                .and("spuAppear").is("0")
                .and("delFlg").is("0")
                .and("shelvesStatus").is("3")
                .and("marketId").is(null)
                .and("cateFid").nin("22196120924543", "22196122839313"));
        query.with(Sort.by(Sort.Direction.DESC, "shelvesAt"));
        query.limit(30);
        List<GoblinGoodsInfoVo> list = mongoTemplate.find(query, GoblinGoodsInfoVo.class, GoblinGoodsInfoVo.class.getSimpleName());

        List<GoblinStoreRecommendGoodsVo> preferred = new ArrayList<>();
        List<GoblinStoreRecommendGoodsVo> fallback = new ArrayList<>();
        for (GoblinGoodsInfoVo goods : list) {
            if (hideSoldOut && isSoldOut(goods)) {
                continue;
            }
            GoblinStoreRecommendGoodsVo vo = new GoblinStoreRecommendGoodsVo();
            vo.setSpuId(goods.getSpuId());
            vo.setName(goods.getName());
            vo.setCoverPic(goods.getCoverPic());
            vo.setPrice(goods.getPriceGe() != null ? goods.getPriceGe() : goods.getSellPrice());
            if (excludeSpuIds.contains(goods.getSpuId())) {
                fallback.add(vo);
            } else {
                preferred.add(vo);
            }
            if (preferred.size() >= 2) {
                break;
            }
        }
        List<GoblinStoreRecommendGoodsVo> result = new ArrayList<>(preferred);
        if (result.size() < 2) {
            for (GoblinStoreRecommendGoodsVo vo : fallback) {
                result.add(vo);
                if (result.size() >= 2) {
                    break;
                }
            }
        }
        return ResponseDto.success(result.size() > 2 ? result.subList(0, 2) : result);
    }

    private boolean isSoldOut(GoblinGoodsInfoVo goods) {
        if (goods.getSkuIdList() == null || goods.getSkuIdList().isEmpty()) {
            return true;
        }
        int count = 0;
        for (String skuId : goods.getSkuIdList()) {
            count += redisUtils.getSkuStock(null, skuId);
        }
        return count <= 0;
    }

    private boolean doSubscribe(String orderId, String mailId, String mailNo, String com, GoblinMailTrackCache cache) {
        if (!kuaidi100Client.isConfigured()) {
            updateSubscribeFail(cache, "快递100未配置");
            redisUtils.setMailTrackCache(mailId, cache);
            return false;
        }
        ensureMonthCounter(cache.getSubscribe());
        if (Boolean.TRUE.equals(cache.getSubscribe().getQuotaExceeded())
                || (cache.getSubscribe().getSubscribeCount() != null && cache.getSubscribe().getSubscribeCount() >= MAX_SUBSCRIBE_PER_MONTH)) {
            cache.getSubscribe().setQuotaExceeded(true);
            cache.getSubscribe().setSubscribeStatus("quota_exceeded");
            cache.getSubscribe().setNeedResubscribe(false);
            redisUtils.setMailTrackCache(mailId, cache);
            return false;
        }
        GoblinStoreOrderVo orderVo = redisUtils.getGoblinOrder(orderId);
        String phone = resolvePhone(orderVo);
        String to = resolveTo(orderVo);
        String from = kuaidi100Client.getDefaultFrom();
        String callback = buildCallbackUrl(mailId, orderId);
        try {
            SubscribeResp resp = kuaidi100Client.subscribeWithMap(com, mailNo, phone, from, to, callback);
            GoblinMailTrackSubscribeVo sub = cache.getSubscribe();
            int count = sub.getSubscribeCount() == null ? 0 : sub.getSubscribeCount();
            sub.setSubscribeCount(count + 1);
            if (sub.getSubscribeCount() >= MAX_SUBSCRIBE_PER_MONTH) {
                sub.setQuotaExceeded(true);
            }
            String now = nowStr();
            sub.setLastSubscribeAt(now);
            if (StringUtils.isBlank(sub.getFirstSubscribeAt())) {
                sub.setFirstSubscribeAt(now);
            }
            if (resp != null && resp.isResult()) {
                sub.setSubscribed(true);
                sub.setSubscribeStatus("polling");
                sub.setNeedResubscribe(false);
                sub.setLastFailReason(null);
                redisUtils.setMailTrackCache(mailId, cache);
                return true;
            }
            String msg = resp == null ? "订阅无响应" : resp.getMessage();
            updateSubscribeFail(cache, msg);
            redisUtils.setMailTrackCache(mailId, cache);
            return false;
        } catch (Exception e) {
            log.error("[mailTrack] subscribe error mailId={}", mailId, e);
            updateSubscribeFail(cache, e.getMessage());
            redisUtils.setMailTrackCache(mailId, cache);
            return false;
        }
    }

    private GoblinMailTrackInfoVo queryAndOverwriteTrack(String orderId, String mailId, String com, String mailNo, String source) {
        if (!kuaidi100Client.isConfigured() || StringUtils.isBlank(com) || StringUtils.isBlank(mailNo)) {
            return null;
        }
        GoblinMailTrackCache cache = redisUtils.getMailTrackCache(mailId);
        if (cache == null) {
            cache = newEmptyCache(orderId, mailId);
        }
        if (shouldDebounce(cache.getTrack())) {
            return cache.getTrack();
        }
        GoblinStoreOrderVo orderVo = redisUtils.getGoblinOrder(orderId);
        String phone = resolvePhone(orderVo);
        String to = resolveTo(orderVo);
        String from = kuaidi100Client.getDefaultFrom();
        try {
            QueryTrackMapResp resp = kuaidi100Client.queryMapTrack(com, mailNo, phone, from, to);
            if (resp == null) {
                return cache.getTrack();
            }
            if (StringUtils.isNotBlank(resp.getReturnCode()) && !"200".equals(resp.getReturnCode()) && !resp.isResult()
                    && (resp.getData() == null || resp.getData().isEmpty())) {
                log.warn("[mailTrack] query fail mailId={}, returnCode={}, message={}", mailId, resp.getReturnCode(), resp.getMessage());
                return cache.getTrack();
            }
            GoblinMailTrackInfoVo track = fromQueryResp(resp, source);
            cache.setTrack(track);
            redisUtils.setMailTrackCache(mailId, cache);
            return track;
        } catch (Exception e) {
            log.error("[mailTrack] queryAndOverwriteTrack error mailId={}", mailId, e);
            return cache.getTrack();
        }
    }

    private boolean shouldDebounce(GoblinMailTrackInfoVo track) {
        if (track == null || StringUtils.isBlank(track.getLastQueryAt()) || queryDebounceSeconds <= 0) {
            return false;
        }
        try {
            LocalDateTime last = LocalDateTime.parse(track.getLastQueryAt(), DateTimeFormatter.ofPattern(DateUtil.DATE_FULL_STR));
            return ChronoUnit.SECONDS.between(last, LocalDateTime.now()) < queryDebounceSeconds;
        } catch (Exception e) {
            return false;
        }
    }

    private GoblinMailTrackCache newEmptyCache(String orderId, String mailId) {
        GoblinMailTrackCache cache = new GoblinMailTrackCache();
        cache.setOrderId(orderId);
        cache.setMailId(mailId);
        GoblinMailTrackSubscribeVo sub = new GoblinMailTrackSubscribeVo();
        sub.setSubscribed(false);
        sub.setNeedResubscribe(true);
        sub.setSubscribeCount(0);
        sub.setSubscribeMonth(LocalDate.now().format(MONTH_FMT));
        sub.setQuotaExceeded(false);
        cache.setSubscribe(sub);
        return cache;
    }

    private void ensureMonthCounter(GoblinMailTrackSubscribeVo sub) {
        String month = LocalDate.now().format(MONTH_FMT);
        if (!month.equals(sub.getSubscribeMonth())) {
            sub.setSubscribeMonth(month);
            sub.setSubscribeCount(0);
            sub.setQuotaExceeded(false);
        }
    }

    private void updateSubscribeFail(GoblinMailTrackCache cache, String reason) {
        GoblinMailTrackSubscribeVo sub = cache.getSubscribe();
        sub.setSubscribed(false);
        sub.setSubscribeStatus("subscribe_failed");
        sub.setNeedResubscribe(true);
        sub.setLastFailReason(reason);
        sub.setLastFailAt(nowStr());
    }

    /**
     * 解析快递公司编码；若 mail 上无编码则 AutoNum，成功后回写 Redis/Mongo，避免老数据反复识别。
     */
    private String resolveAndPersistCom(String orderId, GoblinMailVo mailVo) {
        if (mailVo == null || StringUtils.isBlank(mailVo.getMailNo())) {
            return null;
        }
        if (StringUtils.isNotBlank(mailVo.getLogisticsCode())) {
            return mailVo.getLogisticsCode().trim().toLowerCase();
        }
        AutoNumResp auto = kuaidi100Client.autoComDetail(mailVo.getMailNo());
        if (auto == null || StringUtils.isBlank(auto.getComCode())) {
            return null;
        }
        String com = auto.getComCode().trim().toLowerCase();
        mailVo.setLogisticsCode(com);
        if (StringUtils.isBlank(mailVo.getLogisticsCompany()) && StringUtils.isNotBlank(auto.getName())) {
            mailVo.setLogisticsCompany(auto.getName());
        }
        try {
            GoblinMailVo update = GoblinMailVo.getNew();
            update.setMailId(mailVo.getMailId());
            update.setOrderId(orderId);
            update.setMailNo(mailVo.getMailNo());
            update.setLogisticsCode(mailVo.getLogisticsCode());
            update.setLogisticsCompany(mailVo.getLogisticsCompany());
            redisUtils.changeGoblinMail(orderId, update);
            mongoUtils.updateGoblinMailVo(mailVo.getMailId(), update);
            log.info("[mailTrack] AutoNum persist logisticsCode={}, company={}, mailId={}",
                    com, mailVo.getLogisticsCompany(), mailVo.getMailId());
        } catch (Exception e) {
            log.warn("[mailTrack] persist logisticsCode fail mailId={}", mailVo.getMailId(), e);
        }
        return com;
    }

    private String resolvePhone(GoblinStoreOrderVo orderVo) {
        if (orderVo == null || orderVo.getOrderAttrVo() == null) {
            return "";
        }
        String phone = orderVo.getOrderAttrVo().getExpressPhone();
        if (StringUtils.isBlank(phone)) {
            return "";
        }
        // 电商虚拟号取 - 后四位
        if (phone.contains("-")) {
            String[] parts = phone.split("-");
            return parts[parts.length - 1];
        }
        return phone;
    }

    private String resolveTo(GoblinStoreOrderVo orderVo) {
        if (orderVo == null || orderVo.getOrderAttrVo() == null) {
            return "";
        }
        String address = orderVo.getOrderAttrVo().getExpressAddress();
        return address == null ? "" : address;
    }

    private String buildCallbackUrl(String mailId, String orderId) {
        String base = kuaidi100Client.getCallbackurl();
        String sep = base.contains("?") ? "&" : "?";
        return base + sep + "mailId=" + mailId + "&orderId=" + orderId;
    }

    private GoblinMailVo findMail(String orderId, String mailIdOrNo) {
        List<GoblinMailVo> list = redisUtils.getGoblinMail(orderId);
        if (list == null || StringUtils.isBlank(mailIdOrNo)) {
            return null;
        }
        GoblinMailVo byMailNo = null;
        for (GoblinMailVo mailVo : list) {
            if (mailIdOrNo.equals(mailVo.getMailId())) {
                return mailVo;
            }
            // 兼容前端把 mailNo 当成 mailId 传入（GoblinMailVo.mailId 文档曾误标为「快递单号」）
            if (byMailNo == null && mailIdOrNo.equals(mailVo.getMailNo())) {
                byMailNo = mailVo;
            }
        }
        return byMailNo;
    }

    private GoblinMailTrackInfoVo fromQueryResp(QueryTrackMapResp resp, String source) {
        GoblinMailTrackInfoVo track = new GoblinMailTrackInfoVo();
        track.setState(resp.getState());
        track.setIscheck(resp.getIscheck());
        track.setCom(resp.getCom());
        track.setNu(resp.getNu());
        track.setTrailUrl(resp.getTrailUrl());
        track.setArrivalTime(resp.getArrivalTime());
        track.setRemainTime(resp.getRemainTime());
        track.setTotalTime(resp.getTotalTime());
        track.setRouteInfo(toRouteInfo(resp.getRouteInfo()));
        track.setData(toDataList(resp.getData()));
        track.setLastQueryAt(nowStr());
        track.setUpdatedAt(nowStr());
        track.setSource(source);
        return track;
    }

    private GoblinMailTrackInfoVo fromPushResult(SubscribeWithMapPushResult result, String source) {
        GoblinMailTrackInfoVo track = new GoblinMailTrackInfoVo();
        track.setState(result.getState());
        track.setIscheck(result.getIscheck());
        track.setCom(result.getCom());
        track.setNu(result.getNu());
        track.setTrailUrl(result.getTrailUrl());
        track.setArrivalTime(result.getArrivalTime());
        track.setRemainTime(result.getRemainTime());
        track.setTotalTime(result.getTotalTime());
        track.setRouteInfo(toRouteInfo(result.getRouteInfo()));
        if (result.getData() != null) {
            List<GoblinMailTrackDataVo> dataList = new ArrayList<>();
            for (SubscribePushData d : result.getData()) {
                GoblinMailTrackDataVo vo = new GoblinMailTrackDataVo();
                vo.setTime(d.getTime());
                vo.setFtime(d.getFtime());
                vo.setContext(d.getContext());
                vo.setStatus(d.getStatus());
                vo.setStatusCode(d.getStatusCode());
                vo.setAreaCode(d.getAreaCode());
                vo.setAreaName(d.getAreaName());
                vo.setAreaCenter(d.getAreaCenter());
                vo.setAreaPinYin(d.getAreaPinYin());
                vo.setLocation(d.getLocation());
                dataList.add(vo);
            }
            track.setData(dataList);
        }
        track.setUpdatedAt(nowStr());
        track.setSource(source);
        return track;
    }

    private List<GoblinMailTrackDataVo> toDataList(List<QueryTrackData> data) {
        if (data == null) {
            return Collections.emptyList();
        }
        List<GoblinMailTrackDataVo> list = new ArrayList<>();
        for (QueryTrackData d : data) {
            GoblinMailTrackDataVo vo = new GoblinMailTrackDataVo();
            vo.setTime(d.getTime());
            vo.setFtime(d.getFtime());
            vo.setContext(d.getContext());
            vo.setStatus(d.getStatus());
            vo.setStatusCode(d.getStatusCode());
            vo.setAreaCode(d.getAreaCode());
            vo.setAreaName(d.getAreaName());
            vo.setAreaCenter(d.getAreaCenter());
            vo.setAreaPinYin(d.getAreaPinYin());
            vo.setLocation(d.getLocation());
            list.add(vo);
        }
        return list;
    }

    private GoblinMailTrackRouteInfoVo toRouteInfo(QueryTrackRouteInfo routeInfo) {
        if (routeInfo == null) {
            return null;
        }
        GoblinMailTrackRouteInfoVo vo = new GoblinMailTrackRouteInfoVo();
        vo.setFrom(toPosition(routeInfo.getFrom()));
        vo.setCur(toPosition(routeInfo.getCur()));
        vo.setTo(toPosition(routeInfo.getTo()));
        return vo;
    }

    private GoblinMailTrackPositionVo toPosition(QueryTrackPosition position) {
        if (position == null) {
            return null;
        }
        GoblinMailTrackPositionVo vo = new GoblinMailTrackPositionVo();
        vo.setNumber(position.getNumber());
        vo.setName(position.getName());
        return vo;
    }

    private String nowStr() {
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(DateUtil.DATE_FULL_STR));
    }
}
