package cn.qihangerp.service.external.pdd;

import cn.qihangerp.model.entity.OGoods;
import cn.qihangerp.model.entity.OGoodsSku;
import cn.qihangerp.model.request.ExternalGoodsUpsertRequest;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

/**
 * 将 Canonical 落库结果组装为 {@code pdd.goods.add} 的<strong>单根业务 JSON</strong>（字段名为 POP 下划线形式）；
 * {@link cn.qihangerp.service.external.pdd.PddPopClient#invokeGoodsAdd} 会将其展开为官方网关要求的<strong>表单顶层</strong>字段（与 POP curl / SDK 一致，不使用 {@code param_json}）。
 * <p>拼多多售价：以 SKU {@code retailPrice}（档案建议零售价）为基准，拼单价=建议零售价×{@code external.pdd.group-buy-discount-percent}%，
 * 单买价=拼单价+{@code external.pdd.single-buy-add-fen}（分）；{@code market_price} 取各 SKU 建议零售价（分）之最大（必要时高于单买价以满足校验）。</p>
 *
 * @author guochengyu
 */
@Component
public class PddGoodsAddParamBuilder {

    /**
     * 供发布前拉取类目规则、自动 spec 等使用（与 buildParamJson 内映射一致）。
     */
    public Long resolveCatIdForPublish(String categoryCode, ExternalPddProperties props) {
        return resolveCatId(categoryCode, props);
    }

    public String buildParamJson(OGoods goods, List<OGoodsSku> skus, ExternalGoodsUpsertRequest req,
                                 ExternalPddProperties props, List<ExternalPddProperties.PddSkuOverride> effectiveSkuOverrides,
                                 String catRuleRaw) {
        Long catId = resolveCatId(req.getCategoryCode(), props);
        Long costTpl = resolveCostTemplate(req.getLogisticsTemplateCode(), props);
        if (catId == null || catId <= 0) {
            throw new IllegalStateException("缺少拼多多类目映射：external.pdd.category-map 中未找到 categoryCode="
                    + req.getCategoryCode() + " 或 DEFAULT");
        }
        if (costTpl == null || costTpl <= 0) {
            throw new IllegalStateException("缺少运费模板映射：external.pdd.cost-template-map 中未找到 logisticsTemplateCode="
                    + req.getLogisticsTemplateCode() + " 或 DEFAULT");
        }

        List<OGoodsSku> skuRows = skus == null ? List.of() : skus.stream().filter(s -> s != null && StringUtils.hasText(s.getOuterErpSkuId())).toList();
        if (skuRows.isEmpty()) {
            throw new IllegalStateException("拼多多上架至少需要一条有效 SKU");
        }
        List<ExternalPddProperties.PddSkuOverride> overrides = effectiveSkuOverrides != null ? effectiveSkuOverrides : props.getSkuOverrides();
        if (overrides == null || overrides.size() != skuRows.size()) {
            throw new IllegalStateException("拼多多 SKU 规格映射数量须与 SKU 条数一致：请配置 external.pdd.sku-overrides 或开启自动 spec 解析，当前 SKU 数="
                    + skuRows.size());
        }

        JSONObject root = new JSONObject();
        root.put("auto_fill_spu_property", true);
        // POP 文档示例多为字符串型 cat_id，网关对纯数字字段部分场景校验更稳
        root.put("cat_id", String.valueOf(catId));
        root.put("cost_template_id", costTpl);
        root.put("country_id", props.getDefaultCountryId());
        root.put("goods_type", props.getDefaultGoodsType());
        root.put("goods_name", goods.getName());
        root.put("is_folt", props.isDefaultIsFolt());
        root.put("is_pre_sale", props.isDefaultIsPreSale());
        root.put("is_refundable", props.isDefaultIsRefundable());
        root.put("second_hand", props.isDefaultSecondHand());
        root.put("shipment_limit_second", resolveShipmentSeconds(req, props));
        root.put("two_pieces_discount", props.getTwoPiecesDiscount());

        List<String> carousel = resolveCarousel(goods, req);
        if (carousel.isEmpty()) {
            throw new IllegalStateException("carousel_gallery 不能为空");
        }
        root.put("carousel_gallery", carousel);

        List<String> detail = resolveDetailImages(goods);
        if (detail.isEmpty()) {
            // 平台要求详情图 1~20 张，若无则回退用轮播首张
            detail = List.of(carousel.get(0));
        }
        root.put("detail_gallery", detail);

        long maxSuggestedFen = maxSuggestedRetailFen(skuRows);

        if (StringUtils.hasText(goods.getOuterErpGoodsId())) {
            root.put("out_goods_id", goods.getOuterErpGoodsId());
        }

        JSONArray skuList = new JSONArray();
        String thumbFallback = carousel.get(0);
        long maxSingleFen = 0L;
        for (int i = 0; i < skuRows.size(); i++) {
            OGoodsSku row = skuRows.get(i);
            ExternalPddProperties.PddSkuOverride ov = overrides.get(i);
            if (!StringUtils.hasText(ov.getSpecIdList())) {
                throw new IllegalStateException("external.pdd.sku-overrides[" + i + "].spec-id-list 不能为空");
            }
            long[] gs = groupAndSingleFenFromSuggestedRetail(row.getRetailPrice(), props, row.getOuterErpSkuId());
            long groupFen = gs[0];
            long singleFen = gs[1];
            if (singleFen > maxSingleFen) {
                maxSingleFen = singleFen;
            }

            JSONObject sku = new JSONObject();
            sku.put("is_onsale", 1);
            sku.put("limit_quantity", 999L);
            // POP 文档：sku_list 内 multi_price / price 为分数字符串，如 "1900"、"2200"
            sku.put("multi_price", fenAsSkuPriceString(groupFen));
            sku.put("price", fenAsSkuPriceString(singleFen));
            sku.put("quantity", row.getStockQty() != null ? row.getStockQty().longValue() : 0L);
            sku.put("weight", row.getWeightGram() != null ? row.getWeightGram() : 1000L);
            sku.put("spec_id_list", ov.getSpecIdList());
            sku.put("thumb_url", firstNonBlankImageUrl(row.getSkuImageUrl(), thumbFallback));
            if (StringUtils.hasText(row.getOuterErpSkuId())) {
                sku.put("out_sku_sn", row.getOuterErpSkuId());
            }
            JSONArray skuProps = new JSONArray();
            if (ov.getProperties() != null) {
                for (ExternalPddProperties.PddSkuProperty p : ov.getProperties()) {
                    if (p == null || p.getRefPid() == null) {
                        continue;
                    }
                    JSONObject pj = new JSONObject();
                    pj.put("ref_pid", p.getRefPid());
                    if (p.getVid() != null) {
                        pj.put("vid", p.getVid());
                    }
                    pj.put("value", p.getValue() == null ? "" : p.getValue());
                    pj.put("punit", p.getPunit() == null ? "" : p.getPunit());
                    skuProps.add(pj);
                }
            }
            sku.put("sku_properties", skuProps);
            skuList.add(sku);
        }
        long marketFen = resolvePddMarketPriceFen(maxSuggestedFen, maxSingleFen);
        root.put("market_price", marketFen);
        root.put("sku_list", skuList);
        root.put("goods_properties", buildGoodsProperties(goods, req, props, catRuleRaw));

        return JSON.toJSONString(root);
    }

    /**
     * 在 {@code pdd.goods.add} 根 JSON 上注入拼多多 {@code goods_id}，并移除 {@code out_goods_id}，
     * 供 {@code pdd.goods.information.update} 与 {@code pdd.goods.add} 相同方式展开为<strong>表单顶层</strong>提交。
     */
    public String toInformationUpdateParamJson(String goodsAddRootJson, long pddGoodsId) {
        if (!StringUtils.hasText(goodsAddRootJson)) {
            throw new IllegalStateException("goodsAddRootJson 不能为空");
        }
        JSONObject root = JSON.parseObject(goodsAddRootJson);
        if (root == null) {
            throw new IllegalStateException("无法解析为 JSON 对象");
        }
        root.remove("out_goods_id");
        root.put("goods_id", pddGoodsId);
        return root.toJSONString();
    }

    /**
     * 拼多多「无规格」发品（如图书 {@code input_max_spec_num=0}）：仅一条 {@code sku_list}。
     * 与 POP 文档一致：{@code spec_id_list} 为<strong>字符串</strong>形式的 JSON 数组，无规格为 {@code "[]"}（有规格如 {@code "[25]"}）；
     * {@code price}/{@code multi_price} 为<strong>分数字符串</strong>；不传 {@code spec_detail_list}；根级 {@code is_sku=false}。
     * <p>调用方须保证过滤后仅 1 条有效 SKU。</p>
     */
    public String buildParamJsonNoSpecBook(OGoods goods, List<OGoodsSku> skus, ExternalGoodsUpsertRequest req,
                                           ExternalPddProperties props, String catRuleRaw) {
        Long catId = resolveCatId(req.getCategoryCode(), props);
        Long costTpl = resolveCostTemplate(req.getLogisticsTemplateCode(), props);
        if (catId == null || catId <= 0) {
            throw new IllegalStateException("缺少拼多多类目映射：external.pdd.category-map 中未找到 categoryCode="
                    + req.getCategoryCode() + " 或 DEFAULT");
        }
        if (costTpl == null || costTpl <= 0) {
            throw new IllegalStateException("缺少运费模板映射：external.pdd.cost-template-map 中未找到 logisticsTemplateCode="
                    + req.getLogisticsTemplateCode() + " 或 DEFAULT");
        }
        List<OGoodsSku> skuRows = skus == null ? List.of() : skus.stream()
                .filter(s -> s != null && StringUtils.hasText(s.getOuterErpSkuId())).toList();
        if (skuRows.size() != 1) {
            throw new IllegalStateException("无规格发布仅允许 1 条有效 SKU，当前=" + skuRows.size());
        }
        OGoodsSku row = skuRows.get(0);

        JSONObject root = new JSONObject();
        root.put("auto_fill_spu_property", true);
        root.put("cat_id", String.valueOf(catId));
        root.put("cost_template_id", costTpl);
        root.put("country_id", props.getDefaultCountryId());
        root.put("goods_type", props.getDefaultGoodsType());
        root.put("goods_name", goods.getName());
        root.put("is_folt", props.isDefaultIsFolt());
        root.put("is_pre_sale", props.isDefaultIsPreSale());
        root.put("is_refundable", props.isDefaultIsRefundable());
        root.put("second_hand", props.isDefaultSecondHand());
        root.put("shipment_limit_second", resolveShipmentSeconds(req, props));
        root.put("two_pieces_discount", props.getTwoPiecesDiscount());
        root.put("is_sku", false);

        List<String> carousel = resolveCarousel(goods, req);
        if (carousel.isEmpty()) {
            throw new IllegalStateException("carousel_gallery 不能为空");
        }
        root.put("carousel_gallery", carousel);

        List<String> detail = resolveDetailImages(goods);
        if (detail.isEmpty()) {
            detail = List.of(carousel.get(0));
        }
        root.put("detail_gallery", detail);

        long[] gs = groupAndSingleFenFromSuggestedRetail(row.getRetailPrice(), props, row.getOuterErpSkuId());
        long groupFen = gs[0];
        long singleFen = gs[1];
        long maxSuggestedFen = maxSuggestedRetailFen(skuRows);
        long marketFen = resolvePddMarketPriceFen(maxSuggestedFen, singleFen);

        if (StringUtils.hasText(goods.getOuterErpGoodsId())) {
            root.put("out_goods_id", goods.getOuterErpGoodsId());
        }

        long qty = row.getStockQty() != null ? row.getStockQty().longValue() : 0L;
        root.put("price", singleFen);
        root.put("quantity", qty);

        String desc = resolveGoodsDescForPdd(goods, req);
        if (StringUtils.hasText(desc)) {
            root.put("goods_desc", desc);
        }

        JSONArray skuList = new JSONArray();
        JSONObject sku = new JSONObject();
        sku.put("is_onsale", 1);
        sku.put("limit_quantity", 999L);
        sku.put("multi_price", fenAsSkuPriceString(groupFen));
        sku.put("price", fenAsSkuPriceString(singleFen));
        sku.put("quantity", qty);
        sku.put("weight", row.getWeightGram() != null ? row.getWeightGram() : 1000L);
        sku.put("spec_id_list", "[]");
        sku.put("thumb_url", firstNonBlankImageUrl(row.getSkuImageUrl(), carousel.get(0)));
        if (StringUtils.hasText(row.getOuterErpSkuId())) {
            sku.put("out_sku_sn", row.getOuterErpSkuId());
        }
        sku.put("sku_properties", new JSONArray());
        skuList.add(sku);

        root.put("market_price", marketFen);
        root.put("sku_list", skuList);
        root.put("goods_properties", buildGoodsProperties(goods, req, props, catRuleRaw));

        return JSON.toJSONString(root);
    }

    /**
     * {@code pdd.goods.add} 根级 {@code goods_properties}：
     * <ol>
     *   <li>请求 {@code attributes}（{@code refPid} + {@code value} 或 {@code vid}）</li>
     *   <li>{@code ext.isbn}（及配置的 {@link ExternalPddProperties#getIsbnGoodsPropertyRefPid()}）兼容旧逻辑</li>
     *   <li>若开启 {@link ExternalPddProperties#isFillGoodsPropertiesFromCatRule()} 且传入 {@code pdd.goods.cat.rule.get} 成功体：
     *   解析 {@code goods_properties_rule.properties} 中 {@code required=true} 且非 {@code is_sku} 的项并自动补全（书名、ISBN、枚举单选等）</li>
     * </ol>
     */
    private static JSONArray buildGoodsProperties(OGoods goods, ExternalGoodsUpsertRequest req, ExternalPddProperties props,
                                                String catRuleRaw) {
        JSONArray arr = new JSONArray();
        Set<Long> seenRefPid = new HashSet<>();
        if (req != null && !CollectionUtils.isEmpty(req.getAttributes())) {
            for (ExternalGoodsUpsertRequest.AttributeItem a : req.getAttributes()) {
                if (a == null || a.getRefPid() == null) {
                    continue;
                }
                if (!StringUtils.hasText(a.getValue()) && a.getVid() == null) {
                    continue;
                }
                long rp = a.getRefPid();
                if (!seenRefPid.add(rp)) {
                    continue;
                }
                JSONObject o = new JSONObject();
                o.put("ref_pid", rp);
                if (a.getVid() != null) {
                    o.put("vid", a.getVid());
                }
                String attrVal = a.getValue() == null ? "" : a.getValue().trim();
                if (a.getVid() == null && StringUtils.hasText(attrVal) && props.getIsbnGoodsPropertyRefPid() > 0
                        && rp == props.getIsbnGoodsPropertyRefPid()) {
                    String n = normalizeIsbnForPddGoodsProperty(attrVal);
                    if (StringUtils.hasText(n)) {
                        attrVal = n;
                    }
                }
                o.put("value", attrVal);
                o.put("punit", StringUtils.hasText(a.getUnit()) ? a.getUnit().trim() : "");
                arr.add(o);
            }
        }
        String isbn = resolveIsbnFromExt(req, goods);
        long isbnPid = props.getIsbnGoodsPropertyRefPid();
        if (StringUtils.hasText(isbn) && isbnPid > 0 && seenRefPid.add(isbnPid)) {
            String isbnVal = normalizeIsbnForPddGoodsProperty(isbn);
            if (!StringUtils.hasText(isbnVal)) {
                isbnVal = isbn.trim();
            }
            JSONObject o = new JSONObject();
            o.put("ref_pid", isbnPid);
            o.put("value", isbnVal);
            o.put("punit", "");
            arr.add(o);
        }

        boolean usableRule = props.isFillGoodsPropertiesFromCatRule()
                && StringUtils.hasText(catRuleRaw)
                && !PddOpenApiSupport.isError(catRuleRaw)
                && catRuleRaw.contains("goods_properties_rule");
        List<PddOpenApiSupport.CatGoodsPropertyRuleRow> requiredRows = usableRule
                ? PddOpenApiSupport.listRequiredGoodsLevelPropertyRules(catRuleRaw)
                : List.of();
        if (!requiredRows.isEmpty()) {
            for (PddOpenApiSupport.CatGoodsPropertyRuleRow row : requiredRows) {
                if (seenRefPid.contains(row.getRefPid())) {
                    continue;
                }
                ResolvedGoodsPropValue rv = resolveGoodsPropValueForCatRule(row, goods, req, props);
                if (rv == null || (rv.vid() == null && !StringUtils.hasText(rv.value()))) {
                    continue;
                }
                appendGoodsPropertyEntry(arr, seenRefPid, row.getRefPid(), rv.vid(), rv.value(), "");
            }
            List<String> missing = new ArrayList<>();
            for (PddOpenApiSupport.CatGoodsPropertyRuleRow row : requiredRows) {
                if (!seenRefPid.contains(row.getRefPid())) {
                    missing.add(row.getRefPid() + "[" + (row.getName() == null ? "" : row.getName()) + "]");
                }
            }
            if (!missing.isEmpty()) {
                throw new IllegalStateException("当前类目 goods_properties 必填未补齐（见 pdd.goods.cat.rule.get）："
                        + String.join(", ", missing)
                        + "；请传 attributes(refPid+value 或 vid)、ext.pdd_prop_<refPid>、ext.isbn（ISBN）等");
            }
        }
        return arr;
    }

    private static void appendGoodsPropertyEntry(JSONArray arr, Set<Long> seenRefPid, long refPid, Long vid, String value,
                                                 String punit) {
        if (!seenRefPid.add(refPid)) {
            return;
        }
        JSONObject o = new JSONObject();
        o.put("ref_pid", refPid);
        if (vid != null) {
            o.put("vid", vid);
        }
        o.put("value", value == null ? "" : value);
        o.put("punit", punit == null ? "" : punit);
        arr.add(o);
    }

    private record ResolvedGoodsPropValue(Long vid, String value) {
    }

    private static ResolvedGoodsPropValue resolveGoodsPropValueForCatRule(PddOpenApiSupport.CatGoodsPropertyRuleRow rule,
                                                                          OGoods goods, ExternalGoodsUpsertRequest req,
                                                                          ExternalPddProperties props) {
        if (req != null && !CollectionUtils.isEmpty(req.getAttributes())) {
            for (ExternalGoodsUpsertRequest.AttributeItem a : req.getAttributes()) {
                if (a == null || a.getRefPid() == null || a.getRefPid() != rule.getRefPid()) {
                    continue;
                }
                if (StringUtils.hasText(a.getValue()) || a.getVid() != null) {
                    String val = a.getValue() == null ? "" : a.getValue().trim();
                    if (a.getVid() == null && StringUtils.hasText(val) && props.getIsbnGoodsPropertyRefPid() > 0
                            && rule.getRefPid() == props.getIsbnGoodsPropertyRefPid()) {
                        String n = normalizeIsbnForPddGoodsProperty(val);
                        if (StringUtils.hasText(n)) {
                            val = n;
                        }
                    }
                    return new ResolvedGoodsPropValue(a.getVid(), val);
                }
            }
        }
        JSONArray opts = rule.getValueOptions();
        if (opts != null && !opts.isEmpty()) {
            if (opts.size() == 1) {
                return pickEnumOption(opts.getJSONObject(0));
            }
            String hint = goodsPropertyExtHint(rule, req, goods);
            if (StringUtils.hasText(hint)) {
                ResolvedGoodsPropValue m = matchEnumOption(opts, hint.trim());
                if (m != null) {
                    return m;
                }
            }
            if (props.getIsbnGoodsPropertyRefPid() == rule.getRefPid()) {
                String isbn = resolveIsbnFromExt(req, goods);
                if (StringUtils.hasText(isbn)) {
                    String isbnMatchHint = normalizeIsbnForPddGoodsProperty(isbn);
                    if (!StringUtils.hasText(isbnMatchHint)) {
                        isbnMatchHint = isbn.trim();
                    }
                    ResolvedGoodsPropValue m = matchEnumOption(opts, isbnMatchHint);
                    if (m != null) {
                        return m;
                    }
                }
            }
            return null;
        }
        String v = goodsPropertyExtHint(rule, req, goods);
        if (!StringUtils.hasText(v)) {
            v = heuristicGoodsPropertyText(rule, goods, req, props);
        }
        if (!StringUtils.hasText(v)) {
            return null;
        }
        v = applyMaxValueLen(v, rule.getMaxValueHint());
        v = normalizeIsbnGoodsPropertyValueIfApplicable(rule, props, v);
        return new ResolvedGoodsPropValue(null, v);
    }

    private static ResolvedGoodsPropValue pickEnumOption(JSONObject opt) {
        if (opt == null) {
            return null;
        }
        Long vid = opt.getLong("vid");
        if (vid == null || vid <= 0) {
            vid = opt.getLong("id");
        }
        String val = opt.getString("value");
        if (!StringUtils.hasText(val)) {
            val = opt.getString("name");
        }
        if (vid == null || vid <= 0) {
            vid = null;
        }
        return new ResolvedGoodsPropValue(vid, val == null ? "" : val);
    }

    private static ResolvedGoodsPropValue matchEnumOption(JSONArray opts, String hint) {
        if (opts == null || opts.isEmpty() || !StringUtils.hasText(hint)) {
            return null;
        }
        String h = hint.trim().toLowerCase(Locale.ROOT);
        for (int i = 0; i < opts.size(); i++) {
            JSONObject opt = opts.getJSONObject(i);
            if (opt == null) {
                continue;
            }
            String val = opt.getString("value");
            if (!StringUtils.hasText(val)) {
                val = opt.getString("name");
            }
            if (!StringUtils.hasText(val)) {
                continue;
            }
            String lv = val.trim().toLowerCase(Locale.ROOT);
            if (lv.equals(h) || lv.contains(h) || h.contains(lv)) {
                return pickEnumOption(opt);
            }
        }
        return null;
    }

    private static String goodsPropertyExtHint(PddOpenApiSupport.CatGoodsPropertyRuleRow rule,
                                               ExternalGoodsUpsertRequest req, OGoods goods) {
        String k1 = "pdd_prop_" + rule.getRefPid();
        String v = extFromCanonicalOrReq(req, goods, k1);
        if (StringUtils.hasText(v)) {
            return v;
        }
        if (StringUtils.hasText(rule.getName())) {
            v = extFromCanonicalOrReq(req, goods, compactExtKey(rule.getName()));
            if (StringUtils.hasText(v)) {
                return v;
            }
        }
        return null;
    }

    private static String compactExtKey(String name) {
        return name.replaceAll("\\s+", "").toLowerCase(Locale.ROOT);
    }

    private static String heuristicGoodsPropertyText(PddOpenApiSupport.CatGoodsPropertyRuleRow rule, OGoods goods,
                                                     ExternalGoodsUpsertRequest req, ExternalPddProperties props) {
        String name = rule.getName();
        if (StringUtils.hasText(name)) {
            if (name.contains("ISBN") || name.contains("isbn")) {
                String raw = resolveIsbnFromExt(req, goods);
                if (!StringUtils.hasText(raw)) {
                    return null;
                }
                String n = normalizeIsbnForPddGoodsProperty(raw);
                return StringUtils.hasText(n) ? n : raw.trim();
            }
            if (name.contains("书名")) {
                if (goods != null && StringUtils.hasText(goods.getName())) {
                    return goods.getName().trim();
                }
                if (req != null && StringUtils.hasText(req.getTitle())) {
                    return req.getTitle().trim();
                }
            }
        }
        if (props.getIsbnGoodsPropertyRefPid() == rule.getRefPid()) {
            String raw = resolveIsbnFromExt(req, goods);
            if (!StringUtils.hasText(raw)) {
                return null;
            }
            String n = normalizeIsbnForPddGoodsProperty(raw);
            return StringUtils.hasText(n) ? n : raw.trim();
        }
        return null;
    }

    private static String applyMaxValueLen(String value, String maxValueHint) {
        if (!StringUtils.hasText(value) || !StringUtils.hasText(maxValueHint)) {
            return value;
        }
        try {
            int max = Integer.parseInt(maxValueHint.trim());
            if (max > 0 && value.length() > max) {
                return value.substring(0, max);
            }
        } catch (NumberFormatException ignored) {
        }
        return value;
    }

    private static String extFromCanonicalOrReq(ExternalGoodsUpsertRequest req, OGoods goods, String key) {
        if (!StringUtils.hasText(key)) {
            return null;
        }
        if (req != null && req.getExt() != null) {
            String v = req.getExt().get(key);
            if (StringUtils.hasText(v)) {
                return v.trim();
            }
        }
        if (goods == null || !StringUtils.hasText(goods.getCanonicalExt())) {
            return null;
        }
        try {
            JSONObject c = JSON.parseObject(goods.getCanonicalExt());
            if (c == null) {
                return null;
            }
            JSONObject ext = c.getJSONObject("ext");
            if (ext != null) {
                String v = ext.getString(key);
                if (StringUtils.hasText(v)) {
                    return v.trim();
                }
            }
        } catch (Exception ignored) {
        }
        return null;
    }

    private static String resolveIsbnFromExt(ExternalGoodsUpsertRequest req, OGoods goods) {
        String a = extFromCanonicalOrReq(req, goods, "isbn");
        if (StringUtils.hasText(a)) {
            return a;
        }
        return extFromCanonicalOrReq(req, goods, "ISBN");
    }

    /**
     * 拼多多「ISBN编号」属性通常校验为 13 位纯数字；对横杠/空格、全角数字做清洗，并将合法 ISBN-10 转为 ISBN-13（978 前缀）。
     * <p>13 位时必须满足 ISBN-13（EAN-13）校验位，否则平台会报「格式不正确」。</p>
     */
    private static String normalizeIsbnForPddGoodsProperty(String raw) {
        if (!StringUtils.hasText(raw)) {
            return null;
        }
        String s = normalizeFullWidthAsciiDigits(raw.trim());
        StringBuilder digits = new StringBuilder();
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (c >= '0' && c <= '9') {
                digits.append(c);
            } else if ((c == 'X' || c == 'x') && digits.length() == 9) {
                digits.append('X');
            }
        }
        String compact = digits.toString();
        if (compact.length() == 13 && compact.chars().allMatch(Character::isDigit)) {
            if (!isIsbn13CheckDigitValid(compact)) {
                int expect = computeIsbn13CheckDigit(compact.substring(0, 12));
                throw new IllegalStateException("ISBN-13 校验位错误（拼多多会拒收）：当前=" + compact
                        + "，末位应为 " + expect + "，请核对主档或 ext.isbn 是否为笔误");
            }
            return compact;
        }
        if (compact.length() == 10) {
            String first9 = compact.substring(0, 9);
            if (!first9.chars().allMatch(Character::isDigit)) {
                return null;
            }
            char last = compact.charAt(9);
            if (!(Character.isDigit(last) || last == 'X')) {
                return null;
            }
            return isbn10CompactToIsbn13(first9);
        }
        return null;
    }

    private static String normalizeFullWidthAsciiDigits(String s) {
        StringBuilder sb = new StringBuilder(s.length());
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (c >= '０' && c <= '９') {
                sb.append((char) ('0' + (c - '０')));
            } else {
                sb.append(c);
            }
        }
        return sb.toString();
    }

    /** @param nineDigits ISBN-10 前 9 位（均为数字） */
    private static String isbn10CompactToIsbn13(String nineDigits) {
        String core = "978" + nineDigits;
        return core + computeIsbn13CheckDigit(core);
    }

    /** 前 12 位均为数字时，返回 ISBN-13 第 13 位校验码（0–9）。 */
    private static int computeIsbn13CheckDigit(String twelveDigits) {
        if (twelveDigits == null || twelveDigits.length() != 12) {
            throw new IllegalArgumentException("ISBN-13 校验位计算需要恰好 12 位数字");
        }
        int sum = 0;
        for (int i = 0; i < 12; i++) {
            int d = twelveDigits.charAt(i) - '0';
            if (d < 0 || d > 9) {
                throw new IllegalArgumentException("ISBN-13 校验位计算仅允许数字");
            }
            sum += (i % 2 == 0) ? d : d * 3;
        }
        return (10 - (sum % 10)) % 10;
    }

    private static boolean isIsbn13CheckDigitValid(String isbn13) {
        if (isbn13 == null || isbn13.length() != 13 || !isbn13.chars().allMatch(Character::isDigit)) {
            return false;
        }
        try {
            return computeIsbn13CheckDigit(isbn13.substring(0, 12)) == (isbn13.charAt(12) - '0');
        } catch (IllegalArgumentException e) {
            return false;
        }
    }

    private static String normalizeIsbnGoodsPropertyValueIfApplicable(PddOpenApiSupport.CatGoodsPropertyRuleRow rule,
                                                                      ExternalPddProperties props, String value) {
        if (!StringUtils.hasText(value)) {
            return value;
        }
        boolean isbnRule = props.getIsbnGoodsPropertyRefPid() > 0 && rule.getRefPid() == props.getIsbnGoodsPropertyRefPid();
        if (!isbnRule && StringUtils.hasText(rule.getName())) {
            String n = rule.getName();
            isbnRule = n.contains("ISBN") || n.contains("isbn");
        }
        if (!isbnRule) {
            return value;
        }
        String n = normalizeIsbnForPddGoodsProperty(value);
        return StringUtils.hasText(n) ? n : value;
    }

    private static String resolveGoodsDescForPdd(OGoods goods, ExternalGoodsUpsertRequest req) {
        if (goods != null && StringUtils.hasText(goods.getCanonicalExt())) {
            try {
                JSONObject o = JSON.parseObject(goods.getCanonicalExt());
                if (o != null) {
                    String d = o.getString("goodsDesc");
                    if (!StringUtils.hasText(d)) {
                        d = o.getString("goods_desc");
                    }
                    if (StringUtils.hasText(d)) {
                        return d.trim();
                    }
                }
            } catch (Exception ignored) {
            }
        }
        if (req != null && StringUtils.hasText(req.getTitle())) {
            return req.getTitle().trim();
        }
        if (goods != null && StringUtils.hasText(goods.getName())) {
            return goods.getName().trim();
        }
        return "";
    }

    private static long resolveShipmentSeconds(ExternalGoodsUpsertRequest req, ExternalPddProperties props) {
        if (req.getShipWithinHours() != null && req.getShipWithinHours() > 0) {
            return req.getShipWithinHours() * 3600L;
        }
        return props.getDefaultShipmentLimitSecond();
    }

    /**
     * categoryCode 对应映射须为正数；若键存在但值为 null/≤0，再尝试 {@code DEFAULT}。
     */
    private static Long resolveCatId(String categoryCode, ExternalPddProperties props) {
        Map<String, Long> m = props.getCategoryMap();
        if (m == null || m.isEmpty()) {
            return null;
        }
        Long v = pickPositiveCatId(m, categoryCode);
        if (v != null) {
            return v;
        }
        return pickPositiveCatId(m, "DEFAULT");
    }

    private static Long pickPositiveCatId(Map<String, Long> m, String key) {
        if (!StringUtils.hasText(key) || !m.containsKey(key)) {
            return null;
        }
        Long v = m.get(key);
        return (v != null && v > 0) ? v : null;
    }

    private static Long resolveCostTemplate(String logisticsTemplateCode, ExternalPddProperties props) {
        if (StringUtils.hasText(logisticsTemplateCode) && props.getCostTemplateMap().containsKey(logisticsTemplateCode)) {
            return props.getCostTemplateMap().get(logisticsTemplateCode);
        }
        return props.getCostTemplateMap().get("DEFAULT");
    }

    private static List<String> resolveCarousel(OGoods goods, ExternalGoodsUpsertRequest req) {
        List<String> out = new ArrayList<>();
        addNormalizedCarouselUrl(out, goods != null ? goods.getImage() : null);
        if (req != null && !CollectionUtils.isEmpty(req.getImages())) {
            for (String u : req.getImages()) {
                addNormalizedCarouselUrl(out, u);
            }
        }
        return out;
    }

    /**
     * 主档/前端可能把图片存成 {@code {"url":"...","path":"...","name":"..."}} 整段 JSON；
     * 拼多多 {@code carousel_gallery}/{@code thumb_url} 仅接受可访问的 http(s) 字符串。
     */
    private static String normalizePddImageUrl(String raw) {
        if (!StringUtils.hasText(raw)) {
            return null;
        }
        String t = raw.trim();
        if (t.startsWith("http://") || t.startsWith("https://")) {
            return t;
        }
        if (t.startsWith("{")) {
            try {
                JSONObject o = JSON.parseObject(t);
                if (o != null) {
                    String u = o.getString("url");
                    if (!StringUtils.hasText(u)) {
                        u = o.getString("path");
                    }
                    if (StringUtils.hasText(u)) {
                        return u.trim();
                    }
                }
            } catch (Exception ignored) {
            }
            return null;
        }
        if (t.startsWith("[")) {
            try {
                JSONArray arr = JSON.parseArray(t);
                if (arr != null && !arr.isEmpty()) {
                    Object first = arr.get(0);
                    if (first instanceof String s) {
                        return normalizePddImageUrl(s);
                    }
                    if (first instanceof JSONObject jo) {
                        String u = jo.getString("url");
                        if (!StringUtils.hasText(u)) {
                            u = jo.getString("path");
                        }
                        if (StringUtils.hasText(u)) {
                            return u.trim();
                        }
                    }
                }
            } catch (Exception ignored) {
            }
            return null;
        }
        return t;
    }

    private static void addNormalizedCarouselUrl(List<String> out, String raw) {
        String n = normalizePddImageUrl(raw);
        if (StringUtils.hasText(n) && !out.contains(n)) {
            out.add(n);
        }
    }

    private static String firstNonBlankImageUrl(String primary, String fallback) {
        String a = normalizePddImageUrl(primary);
        if (StringUtils.hasText(a)) {
            return a;
        }
        String b = normalizePddImageUrl(fallback);
        return StringUtils.hasText(b) ? b : "";
    }

    private static List<String> resolveDetailImages(OGoods goods) {
        if (!StringUtils.hasText(goods.getCanonicalExt())) {
            return List.of();
        }
        try {
            JSONObject o = JSON.parseObject(goods.getCanonicalExt());
            if (o == null || !o.containsKey("detailImages")) {
                return List.of();
            }
            JSONArray arr = o.getJSONArray("detailImages");
            if (arr == null || arr.isEmpty()) {
                return List.of();
            }
            List<String> list = new ArrayList<>();
            for (int i = 0; i < arr.size(); i++) {
                Object el = arr.get(i);
                String raw = el instanceof String s ? s : (el != null ? el.toString() : null);
                String u = normalizePddImageUrl(raw);
                if (StringUtils.hasText(u)) {
                    list.add(u);
                }
            }
            return list;
        } catch (Exception e) {
            return List.of();
        }
    }

    /**
     * 定价（market_price）：默认取各 SKU 建议零售价（分）之最大；若低于单买价则抬到单买价+1 分以满足常见校验。
     */
    private static long resolvePddMarketPriceFen(long maxSuggestedRetailFen, long maxSingleBuyFen) {
        if (maxSuggestedRetailFen <= 0) {
            throw new IllegalStateException("无法生成 market_price：各 SKU 建议零售价（retailPrice）无效");
        }
        long m = maxSuggestedRetailFen;
        if (m <= maxSingleBuyFen) {
            return maxSingleBuyFen + 1;
        }
        return m;
    }

    /** 各 SKU 建议零售价（元）换算为分后的最大值 */
    private static long maxSuggestedRetailFen(List<OGoodsSku> skuRows) {
        long max = 0L;
        for (OGoodsSku s : skuRows) {
            if (s == null) {
                continue;
            }
            long fen = yuanToFen(s.getRetailPrice());
            if (fen > max) {
                max = fen;
            }
        }
        return max;
    }

    /**
     * 拼单价（分）= 建议零售价 ×（groupBuyDiscountPercent/100），单买价（分）= 拼单价 + singleBuyAddFen；元→分 HALF_UP。
     */
    private static long[] groupAndSingleFenFromSuggestedRetail(BigDecimal suggestedRetailYuan, ExternalPddProperties props,
                                                               String outSkuIdForError) {
        if (suggestedRetailYuan == null || suggestedRetailYuan.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalStateException("SKU 建议零售价（retailPrice）无效，无法按配置折算拼多多价格，outSkuId=" + outSkuIdForError);
        }
        int pct = props.getGroupBuyDiscountPercent();
        if (pct <= 0 || pct > 100) {
            throw new IllegalStateException("external.pdd.group-buy-discount-percent 须为 1～100，当前=" + pct);
        }
        BigDecimal groupYuan = suggestedRetailYuan.multiply(BigDecimal.valueOf(pct))
                .divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP);
        long groupFen = groupYuan.multiply(BigDecimal.valueOf(100)).setScale(0, RoundingMode.HALF_UP).longValue();
        if (groupFen <= 0) {
            throw new IllegalStateException("折算后拼单价为 0 分，请检查建议零售价与折扣，outSkuId=" + outSkuIdForError);
        }
        long add = props.getSingleBuyAddFen();
        if (add <= 0) {
            throw new IllegalStateException("external.pdd.single-buy-add-fen 须为正整数（分），当前=" + add);
        }
        long singleFen = groupFen + add;
        return new long[]{groupFen, singleFen};
    }

    private static long yuanToFen(BigDecimal yuan) {
        if (yuan == null) {
            return 0L;
        }
        return yuan.multiply(BigDecimal.valueOf(100)).setScale(0, RoundingMode.HALF_UP).longValue();
    }

    /** POP 文档示例中 sku_list 的 price、multi_price 为分数字符串。 */
    private static String fenAsSkuPriceString(long fen) {
        return Long.toString(fen);
    }
}
